"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getSearchableRecordTypes = exports.getSubrecordSearchName = exports.getFirstItem = exports.getPreviousPageSearchDescriptor = exports.getNextPageSearchDescriptor = exports.getListType = exports.searchDescriptorToLocation = exports.advancedSearchConditionToNXQL = exports.fieldConditionToNXQL = exports.rangeFieldConditionToNXQL = exports.structuredDateFieldConditionToNXQL = exports.booleanConditionToNXQL = exports.valueToNXQL = exports.operatorToNXQL = exports.pathToNXQL = exports.normalizePatternValue = exports.normalizeCondition = exports.normalizeFieldCondition = exports.normalizeRangeFieldCondition = exports.normalizeBooleanCondition = exports.normalizeFieldValue = exports.normalizeListFieldValue = exports.normalizeStringFieldValue = exports.normalizeTimestampRangeEndValue = exports.normalizeTimestampRangeStartValue = 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 _recordDataHelpers = require("./recordDataHelpers");

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

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

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

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

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

const normalizeTimestampRangeStartValue = value => {
  if (value) {
    const timestamp = value.trim();

    if (timestamp) {
      return timestamp.indexOf('T') < 0 ? "".concat(timestamp, "T00:00:00.000") : timestamp;
    }
  }

  return null;
};

exports.normalizeTimestampRangeStartValue = normalizeTimestampRangeStartValue;

const normalizeTimestampRangeEndValue = value => {
  if (value) {
    const timestamp = value.trim();

    if (timestamp) {
      return timestamp.indexOf('T') < 0 ? "".concat(timestamp, "T23:59:59.999") : timestamp;
    }
  }

  return null;
};

exports.normalizeTimestampRangeEndValue = normalizeTimestampRangeEndValue;

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) {
    /* Gotta do this mutual recursion */

    /* eslint-disable no-use-before-define */
    childConditions = childConditions.map(childCondition => normalizeCondition(fieldDescriptor, childCondition)).filter(childCondition => !!childCondition);
    /* eslint-enable no-use-before-define */
  }

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

    return childConditions.get(0);
  }

  return null;
};

exports.normalizeBooleanCondition = normalizeBooleanCondition;

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

  if (value) {
    const path = condition.get('path');
    const dataType = getDataType(fieldDescriptor, path);
    let startValue = value.get(0);
    let endValue = value.get(1);

    if (dataType === _dataTypes.DATA_TYPE_DATETIME || dataType === _dataTypes.DATA_TYPE_DATE) {
      startValue = normalizeTimestampRangeStartValue(startValue);
      endValue = normalizeTimestampRangeEndValue(endValue);
    } else {
      startValue = normalizeStringFieldValue(startValue);
      endValue = normalizeStringFieldValue(endValue);
    }

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

    if (!startValue) {
      return _immutable.default.Map({
        op: _searchOperators.OP_LTE,
        path: condition.get('path'),
        value: endValue
      });
    }

    if (!endValue) {
      return _immutable.default.Map({
        op: _searchOperators.OP_GTE,
        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 value = normalizeFieldValue(condition.get('value'));

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

  return null;
};

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:
        return normalizeRangeFieldCondition(fieldDescriptor, condition);

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

  return null;
};

exports.normalizeCondition = normalizeCondition;

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

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

exports.normalizePatternValue = normalizePatternValue;
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'
};

const pathToNXQL = (fieldDescriptor, path) => {
  const [partName, ...pathInPartArray] = path.split('/');
  const nxqlPartName = partName.substr(0, 4) === "".concat(_recordDataHelpers.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 "".concat(nxqlPartName, ":").concat(nxqlPath);
};

exports.pathToNXQL = pathToNXQL;

const operatorToNXQL = operator => operatorToNXQLMap[operator];

exports.operatorToNXQL = operatorToNXQL;

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 \"".concat(nxqlValue, "\"");
  }

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

    nxqlValue = nxqlValue.includes('T') ? nxqlValue : "".concat(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 : "".concat(nxqlValue, "Z");
    return "TIMESTAMP \"".concat(nxqlValue, "\"");
  }

  if (dataType === _dataTypes.DATA_TYPE_INT) {
    nxqlValue = parseInt(data, 10);
  } else if (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) => {
  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 no-use-before-define */
    advancedSearchConditionToNXQL(fieldDescriptor, childCondition))
    /* eslint-enable no-use-before-define */
    .join(" ".concat(nxqlOp, " "));
    return "(".concat(nxql, ")");
  }

  return '';
};

exports.booleanConditionToNXQL = booleanConditionToNXQL;

const structuredDateFieldConditionToNXQL = (fieldDescriptor, condition) => {
  // 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.
  const path = condition.get('path');
  const operator = condition.get('op');
  const value = condition.get('value');
  const earliestScalarDatePath = "".concat(path, "/dateEarliestScalarValue");
  const latestScalarDatePath = "".concat(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({
      op: _searchOperators.OP_AND,
      value: [{
        path: earliestScalarDatePath,
        op: _searchOperators.OP_LTE,
        value: rangeEnd
      }, {
        path: latestScalarDatePath,
        op: _searchOperators.OP_GT,
        value: rangeStart
      }]
    });
  } else if (operator === _searchOperators.OP_CONTAIN) {
    // The structured date range contains the value date.
    convertedCondition = _immutable.default.fromJS({
      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_EQ) {
    // The earliest and latest dates of the structured date are the same, and are equal to the
    // value date.
    convertedCondition = _immutable.default.fromJS({
      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_LT) {
    // The earliest date in the structured date is before the value date, i.e. some part of the
    // structured date exists before the value.
    convertedCondition = _immutable.default.fromJS({
      path: earliestScalarDatePath,
      op: _searchOperators.OP_LT,
      value
    });
  } else if (operator === _searchOperators.OP_LTE) {
    // 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 latest date in the structured date is after the value date, i.e. some part of the
    // structured date exists after the value.
    convertedCondition = _immutable.default.fromJS({
      path: latestScalarDatePath,
      op: _searchOperators.OP_GT,
      // 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_GTE) {
    // 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
    });
  }

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

exports.structuredDateFieldConditionToNXQL = structuredDateFieldConditionToNXQL;

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

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

  const operator = condition.get('op');
  const values = condition.get('value');
  const nxqlPath = pathToNXQL(fieldDescriptor, path);
  const nxqlOp = operatorToNXQL(operator);
  const startValue = values.get(0);
  const endValue = values.get(1);
  const nxqlValue = [startValue, endValue].map(value => valueToNXQL(value, path, fieldDescriptor)).join(' AND ');
  return "".concat(nxqlPath, " ").concat(nxqlOp, " ").concat(nxqlValue);
};

exports.rangeFieldConditionToNXQL = rangeFieldConditionToNXQL;

const fieldConditionToNXQL = (fieldDescriptor, condition) => {
  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))).join(' OR ');
    return "(".concat(orClauses, ")");
  }

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

  if (operator === _searchOperators.OP_CONTAIN) {
    operator = _searchOperators.OP_MATCH;
    value = "%".concat(value, "%");
  }

  if (operator === _searchOperators.OP_MATCH) {
    value = normalizePatternValue(value);
  }

  const nxqlPath = pathToNXQL(fieldDescriptor, path);
  const nxqlOp = operatorToNXQL(operator);
  const nxqlValue = valueToNXQL(value, path, fieldDescriptor);
  return "".concat(nxqlPath, " ").concat(nxqlOp, " ").concat(nxqlValue);
};

exports.fieldConditionToNXQL = fieldConditionToNXQL;

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

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

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

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

  return null;
};
/**
 * Converts a search descriptor to a React Router location.
 */


exports.advancedSearchConditionToNXQL = advancedSearchConditionToNXQL;

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: "?".concat(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 = function getFirstItem(config, listData) {
  let listType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '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/".concat(csid, "/").concat(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.vocabularies;

        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] = Object.assign({}, recordTypeConfig, {
            vocabularies: filteredVocabularies
          });
        }
      }
    } else {
      // Allow other types. These may get filtered down further by child components.
      filteredRecordTypes[recordType] = recordTypeConfig;
    }
  });
  return filteredRecordTypes;
};

exports.getSearchableRecordTypes = getSearchableRecordTypes;