* @return string
*/
private function captureExpression($arg) {
- $chars = str_split($arg);
$isEscaped = $quote = NULL;
$item = '';
$quotes = ['"', "'"];
')' => '(',
];
$enclosures = array_fill_keys($brackets, 0);
- foreach ($chars as $index => $char) {
+ foreach (str_split($arg) as $char) {
if (!$isEscaped && in_array($char, $quotes, TRUE)) {
// Open quotes - we'll ignore everything inside
if (!$quote) {
foreach ($apiParams['select'] ?? [] as $select) {
if (strstr($select, ' AS ')) {
$expr = SqlExpression::convert($select, TRUE);
- $field = $expr->getFields() ? $selectQuery->getField($expr->getFields()[0]) : NULL;
- $joinName = explode('.', $expr->getFields()[0] ?? '')[0];
- $label = $expr::getTitle() . ': ' . (isset($joinMap[$joinName]) ? $joinMap[$joinName] . ' ' : '') . $field['title'];
+ $label = $expr::getTitle();
+ foreach ($expr->getFields() as $num => $fieldName) {
+ $field = $selectQuery->getField($fieldName);
+ $joinName = explode('.', $fieldName)[0];
+ $label .= ($num ? ', ' : ': ') . (isset($joinMap[$joinName]) ? $joinMap[$joinName] . ' ' : '') . $field['title'];
+ }
$calcFields[] = [
'#tag' => 'af-field',
'name' => $expr->getAlias(),
return {field: field, join: join};
}
}
+ function parseFnArgs(info, expr) {
+ var fnName = expr.split('(')[0],
+ argString = expr.substr(fnName.length + 1, expr.length - fnName.length - 2);
+ info.fn = _.find(CRM.crmSearchAdmin.functions, {name: fnName});
+
+ function getKeyword(whitelist) {
+ var keyword;
+ _.each(whitelist, function(flag) {
+ if (argString.indexOf(flag) === 0) {
+ keyword = flag;
+ argString = _.trim(argString.substr(flag.length));
+ return false;
+ }
+ });
+ return keyword;
+ }
+
+ function getExpr() {
+ var expr;
+ if (argString.indexOf('"') === 0) {
+ // Match double-quoted string
+ expr = argString.match(/"([^"\\]|\\.)*"/)[0];
+ } else if (argString.indexOf("'") === 0) {
+ // Match single-quoted string
+ expr = argString.match(/'([^'\\]|\\.)*'/)[0];
+ } else {
+ // Match anything else
+ expr = argString.match(/[^ ,]+/)[0];
+ }
+ if (expr) {
+ argString = _.trim(argString.substr(expr.length));
+ return parseArg(expr);
+ }
+ }
+
+ _.each(info.fn.params, function(param, index) {
+ var exprCount = 0,
+ expr, flagBefore;
+ argString = _.trim(argString);
+ if (!argString.length || (param.name && !getKeyword(param.name))) {
+ return false;
+ }
+ flagBefore = getKeyword(_.keys(param.flag_before || {}));
+ if (param.max_expr) {
+ while (++exprCount <= param.max_expr && argString.length) {
+ expr = getExpr();
+ if (expr) {
+ expr.param = param.name || index;
+ expr.flag_before = flagBefore;
+ info.args.push(expr);
+ }
+ // Only continue if an expression was found and followed by a comma
+ if (!expr || !getKeyword([','])) {
+ break;
+ }
+ }
+ if (expr && !_.isEmpty(expr.flag_after)) {
+ _.last(info.args).flag_after = getKeyword(_.keys(param.flag_after));
+ }
+ }
+ });
+ }
+ // @param {String} arg
+ function parseArg(arg) {
+ arg = _.trim(arg);
+ if (arg && !isNaN(arg)) {
+ return {
+ type: 'number',
+ value: +arg
+ };
+ } else if (_.includes(['"', "'"], arg.substr(0, 1))) {
+ return {
+ type: 'string',
+ value: arg.substr(1, arg.length - 2)
+ };
+ } else if (arg) {
+ var fieldAndJoin = getFieldAndJoin(arg, searchEntity);
+ if (fieldAndJoin) {
+ var split = arg.split(':'),
+ prefixPos = split[0].lastIndexOf(fieldAndJoin.field.name);
+ return {
+ type: 'field',
+ value: arg,
+ path: split[0],
+ field: fieldAndJoin.field,
+ join: fieldAndJoin.join,
+ prefix: prefixPos > 0 ? split[0].substring(0, prefixPos) : '',
+ suffix: !split[1] ? '' : ':' + split[1]
+ };
+ }
+ }
+ }
function parseExpr(expr) {
if (!expr) {
return;
}
var splitAs = expr.split(' AS '),
- info = {fn: null, modifier: '', field: {}, alias: _.last(splitAs)},
- fieldName = splitAs[0],
- bracketPos = splitAs[0].indexOf('(');
- if (bracketPos >= 0) {
- var parsed = splitAs[0].substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
- fieldName = parsed[2];
- info.fn = _.find(CRM.crmSearchAdmin.functions, {name: expr.substring(0, bracketPos)});
- info.modifier = _.trim(parsed[1]);
- }
- var fieldAndJoin = getFieldAndJoin(fieldName, searchEntity);
- if (fieldAndJoin) {
- var split = fieldName.split(':'),
- prefixPos = split[0].lastIndexOf(fieldAndJoin.field.name);
- info.path = split[0];
- info.prefix = prefixPos > 0 ? info.path.substring(0, prefixPos) : '';
- info.suffix = !split[1] ? '' : ':' + split[1];
- info.field = fieldAndJoin.field;
- info.join = fieldAndJoin.join;
+ info = {fn: null, args: [], alias: _.last(splitAs)},
+ bracketPos = expr.indexOf('(');
+ if (bracketPos > 0) {
+ parseFnArgs(info, splitAs[0]);
+ } else {
+ var arg = parseArg(splitAs[0]);
+ if (arg) {
+ arg.param = 0;
+ info.args.push(arg);
+ }
}
return info;
}
function getDefaultLabel(col) {
var info = parseExpr(col),
- label = info.field.label;
+ label = '';
if (info.fn) {
- label = '(' + info.fn.title + ') ' + label;
- }
- if (info.join) {
- label = info.join.label + ': ' + label;
+ label = '(' + info.fn.title + ')';
}
+ _.each(info.args, function(arg) {
+ if (arg.join) {
+ label += (label ? ' ' : '') + arg.join.label + ':';
+ }
+ if (arg.field) {
+ label += (label ? ' ' : '') + arg.field.label;
+ } else {
+ label += (label ? ' ' : '') + arg.value;
+ }
+ });
return label;
}
function fieldToColumn(fieldExpr, defaults) {
var info = parseExpr(fieldExpr),
+ field = _.findWhere(info.args, {type: 'field'}) || {},
values = _.merge({
type: 'field',
key: info.alias,
- dataType: (info.fn && info.fn.dataType) || (info.field && info.field.data_type)
+ dataType: (info.fn && info.fn.dataType) || field.data_type
}, defaults);
if (defaults.label === true) {
values.label = getDefaultLabel(fieldExpr);
}
if (defaults.sortable) {
- values.sortable = (info.field.type === 'Field');
+ values.sortable = field.type === 'Field';
}
return values;
}
},
// Returns name of explicit or implicit join, for links
getJoinEntity: function(info) {
- if (info.field.fk_entity || info.field.name !== info.field.fieldName) {
- return info.prefix + (info.field.fk_entity ? info.field.name : info.field.name.substr(0, info.field.name.lastIndexOf('.')));
- } else if (info.prefix) {
- return info.prefix.replace('.', '');
+ var arg = _.findWhere(info.args, {type: 'field'}) || {},
+ field = arg.field || {};
+ if (field.fk_entity || field.name !== field.fieldName) {
+ return arg.prefix + (field.fk_entity ? field.name : field.name.substr(0, field.name.lastIndexOf('.')));
+ } else if (arg.prefix) {
+ return arg.prefix.replace('.', '');
}
return '';
},
function reconcileAggregateColumns() {
_.each(ctrl.savedSearch.api_params.select, function(col, pos) {
var info = searchMeta.parseExpr(col),
- fieldExpr = info.path + info.suffix;
+ fieldExpr = (_.findWhere(info.args, {type: 'field'}) || {}).value;
if (ctrl.canAggregate(col)) {
// Ensure all non-grouped columns are aggregated if using GROUP BY
if (!info.fn || info.fn.category !== 'aggregate') {
if (!ctrl.savedSearch.api_params.groupBy.length) {
return false;
}
- var info = searchMeta.parseExpr(col);
+ var arg = _.findWhere(searchMeta.parseExpr(col).args, {type: 'field'}) || {};
+ // If the column is not a database field, no
+ if (!arg.field || !arg.field.entity || arg.field.type !== 'Field') {
+ return false;
+ }
// If the column is used for a groupBy, no
- if (ctrl.savedSearch.api_params.groupBy.indexOf(info.path) > -1) {
+ if (ctrl.savedSearch.api_params.groupBy.indexOf(arg.path) > -1) {
return false;
}
// If the entity this column belongs to is being grouped by primary key, then also no
- var idField = searchMeta.getEntity(info.field.entity).primary_key[0];
- return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + idField) < 0;
+ var idField = searchMeta.getEntity(arg.field.entity).primary_key[0];
+ return ctrl.savedSearch.api_params.groupBy.indexOf(arg.prefix + idField) < 0;
};
$scope.fieldsForGroupBy = function() {
var item = {
id: info.alias,
text: ctrl.getFieldLabel(name),
- description: info.field && info.field.description
+ description: info.fn ? info.fn.description : info.args[0].field && info.args[0].field.description
};
if (disabledIf(item.id)) {
item.disabled = true;
// Links to implicit joins
_.each(ctrl.savedSearch.api_params.select, function(fieldName) {
if (!_.includes(fieldName, ' AS ')) {
- var info = searchMeta.parseExpr(fieldName);
+ var info = searchMeta.parseExpr(fieldName).args[0];
if (info.field && !info.suffix && !info.fn && info.field.type === 'Field' && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')),
- idField = searchMeta.parseExpr(idFieldName).field;
+ idField = searchMeta.parseExpr(idFieldName).args[0].field;
if (!ctrl.canAggregate(idFieldName)) {
var joinEntity = searchMeta.getEntity(idField.fk_entity),
label = (idField.join ? idField.join.label + ': ' : '') + (idField.input_attrs && idField.input_attrs.label || idField.label);
}
var info = searchMeta.parseExpr(col.key),
+ field = info.args[0] && info.args[0].field,
value = col.key.split(':')[0];
+ if (!field || info.fn) {
+ delete col.editable;
+ return;
+ }
// If field is an implicit join, use the original fk field
- if (info.field.name !== info.field.fieldName) {
+ if (field.name !== field.fieldName) {
value = value.substr(0, value.lastIndexOf('.'));
info = searchMeta.parseExpr(value);
+ field = info.args[0].field;
}
col.editable = {
- entity: info.field.baseEntity,
- options: !!info.field.options,
- serialize: !!info.field.serialize,
- fk_entity: info.field.fk_entity,
+ entity: field.baseEntity,
+ options: !!field.options,
+ serialize: !!field.serialize,
+ fk_entity: field.fk_entity,
id: info.prefix + 'id',
- name: info.field.name,
+ name: field.name,
value: value
};
};
this.isEditable = function(col) {
var expr = ctrl.getExprFromSelect(col.key),
info = searchMeta.parseExpr(expr);
- return !col.image && !col.rewrite && !col.link && !info.fn && info.field && !info.field.readonly;
+ return !col.image && !col.rewrite && !col.link && !info.fn && info.args[0] && info.args[0].field && !info.args[0].field.readonly;
};
// Aggregate functions (COUNT, AVG, MAX) cannot display as links, except for GROUP_CONCAT
this.getField = function(expr) {
if (!meta[expr]) {
- meta[expr] = searchMeta.parseExpr(expr);
+ meta[expr] = searchMeta.parseExpr(expr).args[0];
}
return meta[expr].field;
};
this.getOptionKey = function(expr) {
if (!meta[expr]) {
- meta[expr] = searchMeta.parseExpr(expr);
+ meta[expr] = _.findWhere(searchMeta.parseExpr(expr).args, {type: 'field'});
}
return meta[expr].suffix ? meta[expr].suffix.slice(1) : 'id';
};
// Output user-facing name/label fields as a link, if possible
function getViewLink(fieldExpr, links) {
var info = searchMeta.parseExpr(fieldExpr),
- entity = searchMeta.getEntity(info.field.entity);
- if (entity && info.field.fieldName === entity.label_field) {
+ firstField = _.findWhere(info.args, {type: 'field'}),
+ entity = firstField && searchMeta.getEntity(firstField.field.entity);
+ if (entity && firstField.field.fieldName === entity.label_field) {
var joinEntity = searchMeta.getJoinEntity(info);
return _.find(links, {join: joinEntity, action: 'view'});
}