"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.clearAdvancedSearchConditionValues = exports.getSearchableRecordTypes = exports.getSubrecordSearchName = exports.getFirstItem = exports.getPreviousPageSearchDescriptor = exports.getNextPageSearchDescriptor = exports.getListType = exports.searchDescriptorToLocation = exports.convertAdvancedSearchConditionToNXQL = exports.advancedSearchConditionToNXQL = exports.createCounter = exports.fieldConditionToNXQL = exports.rangeFieldConditionToNXQL = exports.structuredDateFieldConditionToNXQL = exports.groupConditionToNXQL = exports.booleanConditionToNXQL = exports.valueToNXQL = exports.dateEndTimestamp = exports.dateStartTimestamp = exports.operatorToNXQL = exports.pathToNXQL = exports.patternValueToNXQL = exports.normalizeCondition = exports.normalizeFieldCondition = exports.normalizeRangeFieldCondition = exports.normalizeGroupCondition = exports.normalizeBooleanCondition = exports.normalizeFieldValue = exports.normalizeListFieldValue = exports.normalizeStringFieldValue = exports.dataTypeSupportsMultipleValues = exports.operatorSupportsMultipleValues = exports.operatorExpectsValue = exports.getOperatorsForDataType = void 0;

var _immutable = _interopRequireDefault(require("immutable"));

var _get = _interopRequireDefault(require("lodash/get"));

var _moment = _interopRequireDefault(require("moment"));

var _qs = _interopRequireDefault(require("qs"));

var _warning = _interopRequireDefault(require("warning"));

var _permissionHelpers = require("./permissionHelpers");

var _configHelpers = require("./configHelpers");

var _dataTypes = require("../constants/dataTypes");

var _searchOperators = require("../constants/searchOperators");

var _xmlNames = require("../constants/xmlNames");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const opsByDataType = {
  [_dataTypes.DATA_TYPE_STRING]: [_searchOperators.OP_CONTAIN, _searchOperators.OP_NOT_CONTAIN, _searchOperators.OP_MATCH, _searchOperators.OP_NOT_MATCH, _searchOperators.OP_RANGE, _searchOperators.OP_NOT_RANGE, _searchOperators.OP_GT, _searchOperators.OP_GTE, _searchOperators.OP_LT, _searchOperators.OP_LTE, _searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL],
  [_dataTypes.DATA_TYPE_INT]: [_searchOperators.OP_RANGE, _searchOperators.OP_NOT_RANGE, _searchOperators.OP_GT, _searchOperators.OP_GTE, _searchOperators.OP_LT, _searchOperators.OP_LTE, _searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL],
  [_dataTypes.DATA_TYPE_FLOAT]: [_searchOperators.OP_RANGE, _searchOperators.OP_NOT_RANGE, _searchOperators.OP_GT, _searchOperators.OP_GTE, _searchOperators.OP_LT, _searchOperators.OP_LTE, _searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL],
  [_dataTypes.DATA_TYPE_BOOL]: [_searchOperators.OP_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL],
  [_dataTypes.DATA_TYPE_DATE]: [_searchOperators.OP_RANGE, _searchOperators.OP_NOT_RANGE, _searchOperators.OP_GT, _searchOperators.OP_GTE, _searchOperators.OP_LT, _searchOperators.OP_LTE, _searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL],
  [_dataTypes.DATA_TYPE_DATETIME]: [_searchOperators.OP_RANGE, _searchOperators.OP_NOT_RANGE, _searchOperators.OP_GT, _searchOperators.OP_GTE, _searchOperators.OP_LT, _searchOperators.OP_LTE, _searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL],
  [_dataTypes.DATA_TYPE_STRUCTURED_DATE]: [_searchOperators.OP_RANGE, _searchOperators.OP_NOT_RANGE, _searchOperators.OP_CONTAIN, _searchOperators.OP_NOT_CONTAIN, _searchOperators.OP_GT, _searchOperators.OP_GTC, _searchOperators.OP_LT, _searchOperators.OP_LTC, _searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NOT_COMPLETE, _searchOperators.OP_COMPLETE]
}; // For controlled lists, comparison/range operators will not necessarily produce results that
// users expect, since they are comparing database values/ref names, not display names. Don't
// show those operators on controlled list fields, until we have a way to deal with this.

const controlledListOps = [_searchOperators.OP_EQ, _searchOperators.OP_NOT_EQ, _searchOperators.OP_NULL, _searchOperators.OP_NOT_NULL];

const getOperatorsForDataType = (dataType = _dataTypes.DATA_TYPE_STRING, isControlled) => isControlled ? controlledListOps : opsByDataType[dataType] || [];

exports.getOperatorsForDataType = getOperatorsForDataType;

const operatorExpectsValue = op => op !== _searchOperators.OP_NULL && op !== _searchOperators.OP_NOT_NULL && op !== _searchOperators.OP_COMPLETE && op !== _searchOperators.OP_NOT_COMPLETE;

exports.operatorExpectsValue = operatorExpectsValue;

const operatorSupportsMultipleValues = op => // There is no need to support multiple values with greater than/less than operators, since they
// are redundant. The range search operator could conceivably have multiple values (non-
// overlapping ranges), but the range search input doesn't support multiple values right now.
// This could be implemented if it's needed.
// The below operators allow multiple values.
op === _searchOperators.OP_EQ || op === _searchOperators.OP_CONTAIN || op === _searchOperators.OP_MATCH || op === _searchOperators.OP_RANGE || op === _searchOperators.OP_NOT_EQ || op === _searchOperators.OP_NOT_CONTAIN || op === _searchOperators.OP_NOT_MATCH || op === _searchOperators.OP_NOT_RANGE;

exports.operatorSupportsMultipleValues = operatorSupportsMultipleValues;

const dataTypeSupportsMultipleValues = dataType => // Booleans only have two possible values, so null (don't care) or a single desired
// value is sufficient to describe all searches, and there's no need to allow multiple
// values.
dataType !== _dataTypes.DATA_TYPE_BOOL;

exports.dataTypeSupportsMultipleValues = dataTypeSupportsMultipleValues;

const getDataType = (fieldDescriptor, path) => (0, _get.default)(fieldDescriptor, ['document', ...path.split('/'), _configHelpers.configKey, 'dataType']);

const getSearchCompareField = (fieldDescriptor, path) => (0, _get.default)(fieldDescriptor, ['document', ...path.split('/'), _configHelpers.configKey, 'searchCompareField']);

const getSearchValueTransform = (fieldDescriptor, path) => (0, _get.default)(fieldDescriptor, ['document', ...path.split('/'), _configHelpers.configKey, 'searchTransform']);

const normalizeStringFieldValue = value => {
  let trimmed;

  if (value) {
    trimmed = value.trim();
  }

  return trimmed || null;
};

exports.normalizeStringFieldValue = normalizeStringFieldValue;

const normalizeListFieldValue = list => {
  let filtered;

  if (list) {
    filtered = list.map(value => normalizeStringFieldValue(value)).filter(value => !!value);
  }

  if (!filtered || filtered.size === 0) {
    return null;
  }

  if (filtered.size === 1) {
    return filtered.first();
  }

  return filtered;
};

exports.normalizeListFieldValue = normalizeListFieldValue;

const normalizeFieldValue = value => _immutable.default.List.isList(value) ? normalizeListFieldValue(value) : normalizeStringFieldValue(value);

exports.normalizeFieldValue = normalizeFieldValue;

const normalizeBooleanCondition = (fieldDescriptor, condition) => {
  let childConditions = condition.get('value');

  if (childConditions) {
    childConditions = childConditions // Gotta do this mutual recursion
    // eslint-disable-next-line no-use-before-define
    .map(childCondition => normalizeCondition(fieldDescriptor, childCondition)).filter(childCondition => !!childCondition);
  }

  if (childConditions && childConditions.size > 0) {
    if (childConditions.size > 1) {
      return condition.set('value', childConditions);
    }

    return childConditions.get(0);
  }

  return null;
};

exports.normalizeBooleanCondition = normalizeBooleanCondition;

const normalizeGroupCondition = (fieldDescriptor, condition) => {
  const path = condition.get('path');

  if (!path) {
    return null;
  }

  let childCondition = condition.get('value');

  if (!childCondition) {
    return null;
  }

  const childOp = childCondition.get('op');

  if (childOp !== _searchOperators.OP_AND && childOp !== _searchOperators.OP_OR) {
    // Ensure the child is a boolean operation.
    childCondition = _immutable.default.Map({
      op: _searchOperators.OP_AND,
      value: _immutable.default.List.of(childCondition)
    });
  } // Normalize the child conditions of the child boolean operation.


  let boolChildConditions = childCondition.get('value');

  if (!boolChildConditions) {
    return null;
  }

  boolChildConditions = boolChildConditions // Gotta do this mutual recursion
  // eslint-disable-next-line no-use-before-define
  .map(boolChildCondition => normalizeCondition(fieldDescriptor, boolChildCondition)).filter(boolChildCondition => !!boolChildCondition);

  if (boolChildConditions.size === 0) {
    return null;
  }

  return condition.set('value', childCondition.set('value', boolChildConditions));
};

exports.normalizeGroupCondition = normalizeGroupCondition;

const normalizeRangeFieldCondition = (fieldDescriptor, condition) => {
  const path = condition.get('path');

  if (!path) {
    return null;
  }

  let value = condition.get('value');

  if (value) {
    if (!_immutable.default.List.isList(value)) {
      value = _immutable.default.List.of(value);
    }

    const startValue = normalizeStringFieldValue(value.get(0));
    const endValue = normalizeStringFieldValue(value.get(1));

    if (!startValue && !endValue) {
      return null;
    }

    const op = condition.get('op');
    const dataType = getDataType(fieldDescriptor, path);

    if (!startValue) {
      let newOp;

      if (dataType === _dataTypes.DATA_TYPE_STRUCTURED_DATE) {
        newOp = op === _searchOperators.OP_NOT_RANGE ? _searchOperators.OP_GT : _searchOperators.OP_LTC;
      } else {
        newOp = op === _searchOperators.OP_NOT_RANGE ? _searchOperators.OP_GT : _searchOperators.OP_LTE;
      }

      return _immutable.default.Map({
        op: newOp,
        path: condition.get('path'),
        value: endValue
      });
    }

    if (!endValue) {
      let newOp;

      if (dataType === _dataTypes.DATA_TYPE_STRUCTURED_DATE) {
        newOp = op === _searchOperators.OP_NOT_RANGE ? _searchOperators.OP_LT : _searchOperators.OP_GTC;
      } else {
        newOp = op === _searchOperators.OP_NOT_RANGE ? _searchOperators.OP_LT : _searchOperators.OP_GTE;
      }

      return _immutable.default.Map({
        op: newOp,
        path: condition.get('path'),
        value: startValue
      });
    }

    return condition.set('value', _immutable.default.List([startValue, endValue]));
  }

  return null;
};

exports.normalizeRangeFieldCondition = normalizeRangeFieldCondition;

const normalizeFieldCondition = (fieldDescriptor, condition) => {
  const path = condition.get('path');

  if (!path) {
    return null;
  }

  const value = normalizeFieldValue(condition.get('value'));

  if (value) {
    return condition.set('value', value);
  }

  if (operatorExpectsValue(condition.get('op'))) {
    // The operator expects a value, but none were supplied. Remove the condition.
    return null;
  }

  return condition.delete('value');
};

exports.normalizeFieldCondition = normalizeFieldCondition;

const normalizeCondition = (fieldDescriptor, condition) => {
  if (condition) {
    const operator = condition.get('op');

    switch (operator) {
      case _searchOperators.OP_AND:
      case _searchOperators.OP_OR:
        return normalizeBooleanCondition(fieldDescriptor, condition);

      case _searchOperators.OP_RANGE:
      case _searchOperators.OP_NOT_RANGE:
        return normalizeRangeFieldCondition(fieldDescriptor, condition);

      case _searchOperators.OP_GROUP:
        return normalizeGroupCondition(fieldDescriptor, condition);

      default:
        return normalizeFieldCondition(fieldDescriptor, condition);
    }
  }

  return null;
};

exports.normalizeCondition = normalizeCondition;

const patternValueToNXQL = value => {
  if (!value) {
    return value;
  }

  return value.replace(/(^|(\\\\)+|[^\\])\*+/g, '$1%');
};

exports.patternValueToNXQL = patternValueToNXQL;

const isComparisonOp = op => op === _searchOperators.OP_LT || op === _searchOperators.OP_LTE || op === _searchOperators.OP_GT || op === _searchOperators.OP_GTE || op === _searchOperators.OP_RANGE || op === _searchOperators.OP_NOT_RANGE;

const operatorToNXQLMap = {
  [_searchOperators.OP_AND]: 'AND',
  [_searchOperators.OP_OR]: 'OR',
  [_searchOperators.OP_EQ]: '=',
  [_searchOperators.OP_LT]: '<',
  [_searchOperators.OP_LTE]: '<=',
  [_searchOperators.OP_GT]: '>',
  [_searchOperators.OP_GTE]: '>=',
  [_searchOperators.OP_MATCH]: 'ILIKE',
  [_searchOperators.OP_RANGE]: 'BETWEEN',
  [_searchOperators.OP_NULL]: 'IS NULL',
  [_searchOperators.OP_NOT_EQ]: '<>',
  [_searchOperators.OP_NOT_MATCH]: 'NOT ILIKE',
  [_searchOperators.OP_NOT_RANGE]: 'NOT BETWEEN',
  [_searchOperators.OP_NOT_NULL]: 'IS NOT NULL'
};

const pathToNXQL = (fieldDescriptor, path) => {
  const [partName, ...pathInPartArray] = path.split('/');
  const nxqlPartName = partName.substr(0, 4) === `${_xmlNames.NS_PREFIX}:` ? partName.substr(4) : partName;
  const partDescriptor = (0, _get.default)(fieldDescriptor, ['document', partName]);
  const nxqlPathInPartArray = [];

  for (let i = 0; i < pathInPartArray.length; i += 1) {
    const fieldName = pathInPartArray[i];
    const fieldPath = pathInPartArray.slice(0, i + 1);
    const fieldConfig = (0, _get.default)(partDescriptor, [...fieldPath, _configHelpers.configKey]);
    const repeating = (0, _get.default)(fieldConfig, 'repeating');
    nxqlPathInPartArray.push(repeating ? '*' : fieldName);
  }

  const nxqlPath = nxqlPathInPartArray.join('/');
  return `${nxqlPartName}:${nxqlPath}`;
};

exports.pathToNXQL = pathToNXQL;

const operatorToNXQL = operator => operatorToNXQLMap[operator];

exports.operatorToNXQL = operatorToNXQL;

const dateStartTimestamp = value => value.indexOf('T') < 0 ? `${value}T00:00:00.000` : value;

exports.dateStartTimestamp = dateStartTimestamp;

const dateEndTimestamp = value => value.indexOf('T') < 0 ? `${value}T23:59:59.999` : value;

exports.dateEndTimestamp = dateEndTimestamp;

const valueToNXQL = (value, path, fieldDescriptor) => {
  const dataType = getDataType(fieldDescriptor, path) || _dataTypes.DATA_TYPE_STRING;

  const searchValueTransform = getSearchValueTransform(fieldDescriptor, path);
  let data = value;

  if (searchValueTransform) {
    data = searchValueTransform({
      data: value
    });
  }

  let nxqlValue;

  if (dataType === _dataTypes.DATA_TYPE_DATETIME) {
    // Timestamp values in searches must be given in UTC.
    // Assume timezoneless values are given in local time.
    nxqlValue = new Date(Date.parse(data)).toISOString();
    return `TIMESTAMP "${nxqlValue}"`;
  }

  if (dataType === _dataTypes.DATA_TYPE_DATE) {
    nxqlValue = data; // Append zero time part to date-only timestamps.

    nxqlValue = nxqlValue.includes('T') ? nxqlValue : `${nxqlValue}T00:00:00.000`; // Timestamp values in searches must be given in UTC.
    // Assume timezoneless values are given in UTC.

    nxqlValue = nxqlValue.endsWith('Z') ? nxqlValue : `${nxqlValue}Z`;
    return `TIMESTAMP "${nxqlValue}"`;
  }

  if (dataType === _dataTypes.DATA_TYPE_INT || dataType === _dataTypes.DATA_TYPE_FLOAT) {
    nxqlValue = parseFloat(data);
  } else if (dataType === _dataTypes.DATA_TYPE_BOOL) {
    const boolData = typeof data === 'string' && data === 'false' ? false : !!data;
    nxqlValue = boolData ? 1 : 0;
  } else {
    nxqlValue = data;
  }

  return JSON.stringify(nxqlValue);
};

exports.valueToNXQL = valueToNXQL;

const booleanConditionToNXQL = (fieldDescriptor, condition, counter) => {
  const operator = condition.get('op');
  const nxqlOp = operatorToNXQL(operator);

  if (nxqlOp) {
    const childConditions = condition.get('value');
    const nxql = childConditions.map(childCondition => // Gotta do this mutual recursion
    // eslint-disable-next-line no-use-before-define
    advancedSearchConditionToNXQL(fieldDescriptor, childCondition, counter)).join(` ${nxqlOp} `);
    return `(${nxql})`;
  }

  return '';
};

exports.booleanConditionToNXQL = booleanConditionToNXQL;

const correlatePath = (nxql, nxqlPath, counter) => {
  if (!nxqlPath.endsWith('*')) {
    return nxql;
  }

  const correlationNumber = counter.next();
  const correlatedPath = `${nxqlPath}${correlationNumber}`;
  const nxqlPathMatchString = nxqlPath.replace(/\*/g, '\\*\\d*');
  const correlatedNXQL = nxql.replace(new RegExp(nxqlPathMatchString, 'g'), correlatedPath);
  const index = nxqlPath.lastIndexOf('*', nxqlPath.length - 2);
  return correlatePath(correlatedNXQL, nxqlPath.substring(0, index + 1), counter);
};

const groupConditionToNXQL = (fieldDescriptor, condition, counter) => {
  const path = condition.get('path');
  const childCondition = condition.get('value');
  const nxqlPath = pathToNXQL(fieldDescriptor, path); // Gotta do this mutual recursion
  // eslint-disable-next-line no-use-before-define

  const nxql = advancedSearchConditionToNXQL(fieldDescriptor, childCondition, counter);
  return correlatePath(nxql, nxqlPath, counter);
};

exports.groupConditionToNXQL = groupConditionToNXQL;

const structuredDateFieldConditionToNXQL = (fieldDescriptor, condition, counter) => {
  // Convert structured date searches to searches on the earliest and latest scalar dates.
  // The UI has historically added one day to the latest day when computing the latest scalar
  // date. I don't know why. This has to be taken into account.
  // All conditions that are converted to and/or conditions on the earliest and latest scalar dates
  // need to be correlated to the same group instance, because the field may be repeating. This is
  // done by wrapping them in a group operator.
  const path = condition.get('path');
  const operator = condition.get('op');
  const value = condition.get('value');
  const earliestScalarDatePath = `${path}/dateEarliestScalarValue`;
  const latestScalarDatePath = `${path}/dateLatestScalarValue`;
  let convertedCondition;

  if (operator === _searchOperators.OP_RANGE) {
    // The structured date range overlaps the value range.
    const rangeStart = value.get(0);
    const rangeEnd = value.get(1);
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_AND,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_LTE,
          value: rangeEnd
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_GT,
          // Not GTE, because latest scalar date has one day added.
          value: rangeStart
        }]
      }
    });
  } else if (operator === _searchOperators.OP_NOT_RANGE) {
    // The structured date range does not overlap the value range.
    // This will be the logical negation of OP_RANGE.
    const rangeStart = value.get(0);
    const rangeEnd = value.get(1);
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_OR,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_GT,
          value: rangeEnd
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_LTE,
          // Not LT, because latest scalar date has one day added.
          value: rangeStart
        }]
      }
    });
  } else if (operator === _searchOperators.OP_CONTAIN) {
    // The structured date range contains the value date.
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_AND,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_LTE,
          value
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_GT,
          // Not GTE, because latest scalar date has one day added.
          value
        }]
      }
    });
  } else if (operator === _searchOperators.OP_NOT_CONTAIN) {
    // The structured date range does not contain the value date.
    // This will be the logical negation of OP_CONTAIN.
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_OR,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_GT,
          value
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_LTE,
          // Not LT, because latest scalar date has one day added.
          value
        }]
      }
    });
  } else if (operator === _searchOperators.OP_EQ) {
    // The earliest and latest dates of the structured date are the same, and are equal to the
    // value date.
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_AND,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_EQ,
          value
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_EQ,
          // GRRR. The latest scalar date has one day added.
          value: (0, _moment.default)(value).add(1, 'day').format('YYYY-MM-DD')
        }]
      }
    });
  } else if (operator === _searchOperators.OP_NOT_EQ) {
    // Either the earliest or latest dates of the structured date are not equal to the
    // value date.
    // This will be the logical negation of OP_EQ.
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_OR,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_NOT_EQ,
          value
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_NOT_EQ,
          // GRRR. The latest scalar date has one day added.
          value: (0, _moment.default)(value).add(1, 'day').format('YYYY-MM-DD')
        }]
      }
    });
  } else if (operator === _searchOperators.OP_LT) {
    // The latest date in the structured date is before the value date, i.e. no part of the
    // structured date exists on or after the value.
    convertedCondition = _immutable.default.fromJS({
      path: latestScalarDatePath,
      op: _searchOperators.OP_LTE,
      // Not LT, because latest scalar date has one day added.
      value
    });
  } else if (operator === _searchOperators.OP_LTC) {
    // The earliest date in the structured date is before or equal to the value date.
    convertedCondition = _immutable.default.fromJS({
      path: earliestScalarDatePath,
      op: _searchOperators.OP_LTE,
      value
    });
  } else if (operator === _searchOperators.OP_GT) {
    // The earliest date in the structured date is after the value date, i.e. no part of the
    // structured date exists on or before the value.
    convertedCondition = _immutable.default.fromJS({
      path: earliestScalarDatePath,
      op: _searchOperators.OP_GT,
      value
    });
  } else if (operator === _searchOperators.OP_GTC) {
    // The latest date in the structured date is after or equal to the value date.
    convertedCondition = _immutable.default.fromJS({
      path: latestScalarDatePath,
      op: _searchOperators.OP_GT,
      // Not GTE, because latest scalar date has one day added.
      value
    });
  } else if (operator === _searchOperators.OP_COMPLETE) {
    // Both the earliest and latest scalar date are not null.
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_AND,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_NOT_NULL
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_NOT_NULL
        }]
      }
    });
  } else if (operator === _searchOperators.OP_NOT_COMPLETE) {
    // Either the earliest or latest scalar date is null.
    // This will be the logical negation of OP_COMPLETE.
    convertedCondition = _immutable.default.fromJS({
      path,
      op: _searchOperators.OP_GROUP,
      value: {
        op: _searchOperators.OP_OR,
        value: [{
          path: earliestScalarDatePath,
          op: _searchOperators.OP_NULL
        }, {
          path: latestScalarDatePath,
          op: _searchOperators.OP_NULL
        }]
      }
    });
  }

  process.env.NODE_ENV !== "production" ? (0, _warning.default)(convertedCondition, `The operator ${operator} is not supported for structured date fields. Search condition will be ignored.`) : void 0;
  return convertedCondition // Gotta do this mutual recursion
  // eslint-disable-next-line no-use-before-define
  ? advancedSearchConditionToNXQL(fieldDescriptor, convertedCondition, counter) : null;
};

exports.structuredDateFieldConditionToNXQL = structuredDateFieldConditionToNXQL;

const resolveSearchCompareField = (fieldDescriptor, condition) => {
  const op = condition.get('op');
  const path = condition.get('path');

  if (isComparisonOp(op)) {
    const searchCompareField = getSearchCompareField(fieldDescriptor, path);

    if (searchCompareField) {
      return condition.set('path', searchCompareField);
    }
  }

  return condition;
};

const rangeFieldConditionToNXQL = (fieldDescriptor, rangeFieldCondition, counter) => {
  const condition = resolveSearchCompareField(fieldDescriptor, rangeFieldCondition);
  const path = condition.get('path');
  const dataType = getDataType(fieldDescriptor, path);

  if (dataType === _dataTypes.DATA_TYPE_STRUCTURED_DATE) {
    return structuredDateFieldConditionToNXQL(fieldDescriptor, condition, counter);
  }

  const operator = condition.get('op');
  const values = condition.get('value');
  const nxqlPath = pathToNXQL(fieldDescriptor, path);
  const nxqlOp = operatorToNXQL(operator);
  let startValue = values.get(0);
  let endValue = values.get(1);

  if (dataType === _dataTypes.DATA_TYPE_DATETIME) {
    // } || dataType === DATA_TYPE_DATE) {
    startValue = dateStartTimestamp(startValue);
    endValue = dateEndTimestamp(endValue);
  }

  const nxqlValue = [startValue, endValue].map(value => valueToNXQL(value, path, fieldDescriptor)).join(' AND ');
  return `${nxqlPath} ${nxqlOp} ${nxqlValue}`;
};

exports.rangeFieldConditionToNXQL = rangeFieldConditionToNXQL;

const fieldConditionToNXQL = (fieldDescriptor, fieldCondition, counter) => {
  const condition = resolveSearchCompareField(fieldDescriptor, fieldCondition);
  const path = condition.get('path');
  const dataType = getDataType(fieldDescriptor, path);
  let operator = condition.get('op');
  let value = condition.get('value');

  if (_immutable.default.List.isList(value)) {
    // Expand or'ed values.
    const orClauses = value.map(valueInstance => fieldConditionToNXQL(fieldDescriptor, condition.set('value', valueInstance), counter)).join(' OR ');
    return `(${orClauses})`;
  }

  if (dataType === _dataTypes.DATA_TYPE_STRUCTURED_DATE) {
    return structuredDateFieldConditionToNXQL(fieldDescriptor, condition, counter);
  }

  if (dataType === _dataTypes.DATA_TYPE_DATETIME) {
    // } || dataType === DATA_TYPE_DATE) {
    if (operator === _searchOperators.OP_EQ || operator === _searchOperators.OP_NOT_EQ) {
      return rangeFieldConditionToNXQL(fieldDescriptor, condition.set('op', operator === _searchOperators.OP_EQ ? _searchOperators.OP_RANGE : _searchOperators.OP_NOT_RANGE).set('value', _immutable.default.List([value, value])), counter);
    }

    if (operator === _searchOperators.OP_GT || operator === _searchOperators.OP_LTE) {
      value = dateEndTimestamp(value);
    } else {
      value = dateStartTimestamp(value);
    }
  }

  if (operator === _searchOperators.OP_CONTAIN) {
    operator = _searchOperators.OP_MATCH;
    value = `%${value}%`;
  } else if (operator === _searchOperators.OP_NOT_CONTAIN) {
    operator = _searchOperators.OP_NOT_MATCH;
    value = `%${value}%`;
  }

  if (operator === _searchOperators.OP_MATCH || operator === _searchOperators.OP_NOT_MATCH) {
    value = patternValueToNXQL(value);
  }

  const nxqlPath = pathToNXQL(fieldDescriptor, path);
  const nxqlOp = operatorToNXQL(operator);

  if (typeof value === 'undefined') {
    return `${nxqlPath} ${nxqlOp}`;
  }

  const nxqlValue = valueToNXQL(value, path, fieldDescriptor);
  return `${nxqlPath} ${nxqlOp} ${nxqlValue}`;
};

exports.fieldConditionToNXQL = fieldConditionToNXQL;

const createCounter = () => {
  let nextNum = 1;
  return {
    next: () => {
      const result = nextNum;
      nextNum += 1;
      return result;
    }
  };
};

exports.createCounter = createCounter;

const advancedSearchConditionToNXQL = (fieldDescriptor, condition, counter) => {
  if (condition) {
    const operator = condition.get('op');

    switch (operator) {
      case _searchOperators.OP_AND:
      case _searchOperators.OP_OR:
        return booleanConditionToNXQL(fieldDescriptor, condition, counter);

      case _searchOperators.OP_RANGE:
      case _searchOperators.OP_NOT_RANGE:
        return rangeFieldConditionToNXQL(fieldDescriptor, condition, counter);

      case _searchOperators.OP_GROUP:
        return groupConditionToNXQL(fieldDescriptor, condition, counter);

      default:
        return fieldConditionToNXQL(fieldDescriptor, condition, counter);
    }
  }

  return null;
};

exports.advancedSearchConditionToNXQL = advancedSearchConditionToNXQL;

const convertAdvancedSearchConditionToNXQL = (fieldDescriptor, condition) => advancedSearchConditionToNXQL(fieldDescriptor, condition, createCounter());
/**
 * Converts a search descriptor to a React Router location.
 */


exports.convertAdvancedSearchConditionToNXQL = convertAdvancedSearchConditionToNXQL;

const searchDescriptorToLocation = searchDescriptor => {
  const recordType = searchDescriptor.get('recordType');
  const vocabulary = searchDescriptor.get('vocabulary');
  const csid = searchDescriptor.get('csid');
  const subresource = searchDescriptor.get('subresource');
  const searchQuery = searchDescriptor.get('searchQuery');
  const pathParts = ['/list', recordType, vocabulary, csid, subresource];
  const pathname = pathParts.filter(part => !!part).join('/');
  const as = searchQuery.get('as');
  const p = searchQuery.get('p');
  const asParam = as ? JSON.stringify(as.toJS()) : undefined;
  const pParam = typeof p === 'number' ? (p + 1).toString() : undefined;

  const queryString = _qs.default.stringify(searchQuery.set('as', asParam).set('p', pParam).toJS());

  return {
    pathname,
    search: `?${queryString}`
  };
};

exports.searchDescriptorToLocation = searchDescriptorToLocation;

const getListType = (config, searchDescriptor) => {
  if (searchDescriptor) {
    const subresource = searchDescriptor.get('subresource');

    if (subresource) {
      return (0, _get.default)(config, ['subresources', subresource, 'listType']);
    }
  }

  return 'common';
};
/**
 * Returns a search descriptor that describes the next page of the search described by a given
 * descriptor.
 */


exports.getListType = getListType;

const getNextPageSearchDescriptor = searchDescriptor => {
  const p = searchDescriptor.getIn(['searchQuery', 'p']) || 0;
  return searchDescriptor.setIn(['searchQuery', 'p'], p + 1);
};
/**
 * Returns a search descriptor that describes the previous page of the search described by a given
 * descriptor. Null is returned if the given descriptor is on the first page.
 */


exports.getNextPageSearchDescriptor = getNextPageSearchDescriptor;

const getPreviousPageSearchDescriptor = searchDescriptor => {
  const p = searchDescriptor.getIn(['searchQuery', 'p']) || 0;

  if (p <= 0) {
    return null;
  }

  return searchDescriptor.setIn(['searchQuery', 'p'], p - 1);
};
/**
 * Returns the first item in list result data.
 */


exports.getPreviousPageSearchDescriptor = getPreviousPageSearchDescriptor;

const getFirstItem = (config, listData, listType = 'common') => {
  if (!listData) {
    return null;
  }

  const listTypeConfig = (0, _get.default)(config, ['listTypes', listType]);

  if (!listTypeConfig) {
    return null;
  }

  const {
    listNodeName,
    itemNodeName
  } = listTypeConfig;
  const items = listData.getIn([listNodeName, itemNodeName]);
  const item = _immutable.default.List.isList(items) ? items.first() : items;
  return item;
};
/**
 * Returns a name for a search for subrecords.
 */


exports.getFirstItem = getFirstItem;

const getSubrecordSearchName = (csid, subrecordName) => `subrecord/${csid}/${subrecordName}`;

exports.getSubrecordSearchName = getSubrecordSearchName;

const getSearchableRecordTypes = (getAuthorityVocabCsid, config, perms) => {
  const {
    recordTypes
  } = config;
  const filteredRecordTypes = {}; // Filter out record types for which the user doesn't have suffiencient permissions, and
  // vocabularies that don't exist on the server.

  Object.keys(recordTypes).forEach(recordType => {
    const recordTypeConfig = recordTypes[recordType];
    const serviceType = (0, _get.default)(recordTypeConfig, ['serviceConfig', 'serviceType']);

    if (serviceType === 'object' || serviceType === 'procedure') {
      // For objects and procedures, check if list permissions exist.
      if ((0, _permissionHelpers.canList)(recordType, perms)) {
        filteredRecordTypes[recordType] = recordTypeConfig;
      }
    } else if (serviceType === 'authority') {
      // For authorities, check if list permissions exist, and if so, filter the vocabularies.
      if ((0, _permissionHelpers.canList)(recordType, perms)) {
        const {
          vocabularies
        } = recordTypeConfig;

        if (vocabularies) {
          const filteredVocabularies = {}; // Filter out vocabularies that are configured in the UI, but don't exist in the services
          // layer. This will happen when a vocabulary has been configured in the UI, but hasn't
          // been created/initialized through the REST API. This isn't necessarily a bug, because
          // sometimes vocbularies are defined in the UI for convenience, but aren't expected to be
          // created in all tenants.

          Object.keys(vocabularies).forEach(vocabulary => {
            const vocabularyConfig = vocabularies[vocabulary];
            const {
              type
            } = vocabularyConfig; // The 'all' vocabulary always exists, so always let that through the filter. For
            // others, check if a csid for the vocabulary was successfully retrieved from the
            // services layer.

            const exists = type === 'all' || getAuthorityVocabCsid(recordType, vocabulary);

            if (exists) {
              filteredVocabularies[vocabulary] = vocabularyConfig;
            }
          });
          filteredRecordTypes[recordType] = { ...recordTypeConfig,
            vocabularies: filteredVocabularies
          };
        }
      }
    } else {
      // Allow other types. These may get filtered down further by child components.
      filteredRecordTypes[recordType] = recordTypeConfig;
    }
  });
  return filteredRecordTypes;
};

exports.getSearchableRecordTypes = getSearchableRecordTypes;

const clearAdvancedSearchConditionValues = condition => {
  if (!condition) {
    return condition;
  }

  const op = condition.get('op');
  const value = condition.get('value');

  if (op === _searchOperators.OP_AND || op === _searchOperators.OP_OR) {
    if (_immutable.default.List.isList(value)) {
      return condition.set('value', value.map(childCondition => clearAdvancedSearchConditionValues(childCondition)));
    }

    return condition.set('value', clearAdvancedSearchConditionValues(value));
  }

  if (op === _searchOperators.OP_GROUP) {
    return condition.set('value', clearAdvancedSearchConditionValues(value));
  }

  return condition.delete('value');
};

exports.clearAdvancedSearchConditionValues = clearAdvancedSearchConditionValues;