'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.hasHierarchyRelations = exports.isRecordImmutable = exports.isSecurityRecordImmutable = exports.isRecordReplicated = exports.isRecordLocked = exports.isRecordDeprecated = exports.getWorkflowState = exports.isNewRecord = exports.isExistingRecord = exports.computeRecordData = exports.computeField = exports.validateRecordData = exports.validateField = exports.getCreatedUser = exports.getCreatedTimestamp = exports.getUpdatedUser = exports.getUpdatedTimestamp = exports.getRefName = exports.getCsid = exports.getCoreFieldValue = exports.prepareForSending = exports.setXmlNamespaceAttribute = exports.attributePropertiesToTop = exports.getDocument = exports.cloneRecordData = exports.clearUncloneable = exports.createRecordData = exports.applyDefaults = exports.spreadDefaultValue = exports.createBlankRecord = exports.normalizeRecordData = exports.normalizeFieldValue = exports.deepDelete = exports.deepSet = exports.deepGet = exports.getPartNSPropertyName = exports.getPart = exports.getPartPropertyName = exports.ERROR_KEY = exports.DOCUMENT_PROPERTY_NAME = exports.NS_PREFIX = undefined;

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _immutable = require('immutable');

var _immutable2 = _interopRequireDefault(_immutable);

var _get = require('lodash/get');

var _get2 = _interopRequireDefault(_get);

var _errorCodes = require('../constants/errorCodes');

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

var _csidHelpers = require('./csidHelpers');

var _workflowStateHelpers = require('./workflowStateHelpers');

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

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }

var numericPattern = /^[0-9]$/;

var NS_PREFIX = exports.NS_PREFIX = 'ns2';
var DOCUMENT_PROPERTY_NAME = exports.DOCUMENT_PROPERTY_NAME = 'document';
var ERROR_KEY = exports.ERROR_KEY = '[error]';

var getPartPropertyName = exports.getPartPropertyName = function getPartPropertyName(partName) {
  return NS_PREFIX + ':' + partName;
};

var getPart = exports.getPart = function getPart(data, partName) {
  return data.getIn([DOCUMENT_PROPERTY_NAME, getPartPropertyName(partName)]);
};

var getPartNSPropertyName = exports.getPartNSPropertyName = function getPartNSPropertyName(prefix) {
  return '@xmlns:' + prefix;
};

/**
 * Deeply get a value in an Immutable.Map. This is similar to Immutable.Map.getIn, but differs in
 * one way:
 *
 * When a key of '0' is encountered, and that key is used to index a data item that is not a List,
 * the data item itself is returned. This accommodates data in which a list containing a single
 * item may be represented by that one item.
 */
var deepGet = exports.deepGet = function deepGet(data, path) {
  if (!Array.isArray(path) || path.length === 0) {
    throw new Error('path must be a non-empty array');
  }

  if (!data) {
    return undefined;
  }

  var _path = _toArray(path),
      key = _path[0],
      rest = _path.slice(1);

  var value = void 0;

  if ((key === '0' || key === 0) && !_immutable2.default.List.isList(data)) {
    // Allow a key of 0 to refer to a single non-list value.
    value = data;
  } else {
    value = data.get(key);
  }

  if (!value || rest.length === 0) {
    return value;
  }

  return deepGet(value, rest);
};

/**
 * Deeply set a value in an Immutable.Map. This is similar to Immutable.Map.setIn, but differs in
 * two ways:
 *
 * When a non-existent key is encountered in the middle of a path, this function may create a List
 * or a Map at that location, depending on the key. If the key is a numeric string, a List is
 * created. Otherwise, a Map is created. Immutable.Map.setIn always creates a Map.
 *
 * This function also promotes an existing singular (non-List) item to a List, if any numeric key
 * is applied to it.
 */
var deepSet = exports.deepSet = function deepSet(data, path, value) {
  if (!Array.isArray(path) || path.length === 0) {
    throw new Error('path must be a non-empty array');
  }

  var _path2 = _toArray(path),
      key = _path2[0],
      rest = _path2.slice(1);

  var isKeyNumeric = numericPattern.test(key);

  var normalizedData = void 0;

  if (data) {
    if (isKeyNumeric && !_immutable2.default.List.isList(data)) {
      // Promote a single (non-list) value into a list when a numeric key is supplied.
      normalizedData = _immutable2.default.List.of(data);
    } else {
      normalizedData = data;
    }
  } else if (isKeyNumeric) {
    normalizedData = _immutable2.default.List();
  } else {
    normalizedData = _immutable2.default.Map();
  }

  var resolvedValue = rest.length === 0 ? value : deepSet(normalizedData.get(key), rest, value);

  return normalizedData.set(key, resolvedValue);
};

/**
 * Deeply delete a value in an Immutable.Map. This is similar to Immutable.Map.deleteIn, but differs
 * in two ways:
 *
 * When a non-existent key is encountered in the middle of a path, this function may create a List
 * or a Map at that location, depending on the key. If the key is a numeric string, a List is
 * created. Otherwise, a Map is created. Immutable.Map.deleteIn makes no change when a key in the
 * path does not exist.
 *
 * This function also promotes an existing singular (non-List) item to a List, if any numeric key
 * is applied to it.
 */
var deepDelete = exports.deepDelete = function deepDelete(data, path) {
  return (
    // First call deepSet with undefined value to ensure the path exists. Then call deleteIn.
    deepSet(data, path).deleteIn(path)
  );
};

var normalizeFieldValue = exports.normalizeFieldValue = function normalizeFieldValue(fieldDescriptor, fieldValue) {
  var expandRepeating = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;

  var normalizedValue = fieldValue;

  if (fieldDescriptor && typeof fieldValue !== 'undefined') {
    if (_immutable2.default.Map.isMap(normalizedValue)) {
      normalizedValue = normalizedValue.map(function (childValue, childName) {
        return normalizeFieldValue(fieldDescriptor[childName], childValue);
      });
    } else if (_immutable2.default.List.isList(normalizedValue)) {
      normalizedValue = normalizedValue.map(function (instance) {
        return normalizeFieldValue(fieldDescriptor, instance, false);
      });
    }

    if (expandRepeating && (0, _configHelpers.isFieldRepeating)(fieldDescriptor) && !_immutable2.default.List.isList(normalizedValue)) {
      normalizedValue = _immutable2.default.List.of(normalizedValue);
    }
  }

  return normalizedValue;
};

var normalizeRecordData = exports.normalizeRecordData = function normalizeRecordData(recordTypeConfig, data) {
  var normalizedData = normalizeFieldValue(recordTypeConfig.fields, data);

  if (recordTypeConfig.normalizeRecordData) {
    normalizedData = recordTypeConfig.normalizeRecordData(data);
  }

  return normalizedData;
};

/**
 * Create a blank data record for a given CollectionSpace record type.
 */
var createBlankRecord = exports.createBlankRecord = function createBlankRecord(recordTypeConfig) {
  var fields = recordTypeConfig.fields;


  var documentKey = Object.keys(fields)[0];
  var documentDescriptor = fields[documentKey];
  var document = {};

  // On some records, e.g. roles, the document has a namespace.

  var documentNsUri = (0, _get2.default)(documentDescriptor, [_configHelpers.configKey, 'service', 'ns']);

  if (documentNsUri) {
    var nsPrefix = documentKey.split(':', 2)[0];

    document[getPartNSPropertyName(nsPrefix)] = documentNsUri;
  }

  // Most records have parts with a namespace.

  var partKeys = Object.keys(documentDescriptor);

  partKeys.forEach(function (partKey) {
    var partDescriptor = documentDescriptor[partKey];
    var nsUri = (0, _get2.default)(partDescriptor, [_configHelpers.configKey, 'service', 'ns']);

    if (nsUri) {
      var _nsPrefix = partKey.split(':', 2)[0];

      document[partKey] = _defineProperty({}, getPartNSPropertyName(_nsPrefix), nsUri);
    }
  });

  return _immutable2.default.fromJS(_defineProperty({}, documentKey, document));
};

/**
 * Set a default value into record data. When declared on a repeating field, a default value will
 * be set on all instances of that field existing in the record data. If a repeating field has no
 * instances, a single instance will be created.
 */
var spreadDefaultValue = exports.spreadDefaultValue = function spreadDefaultValue(value, fieldDescriptorPath, data) {
  if (!fieldDescriptorPath || fieldDescriptorPath.length === 0) {
    return typeof data === 'undefined' ? value : data;
  }

  var map = void 0;

  if (typeof data === 'undefined') {
    map = _immutable2.default.Map();
  } else if (_immutable2.default.Map.isMap(data)) {
    map = data;
  } else {
    return data;
  }

  var _fieldDescriptorPath = _toArray(fieldDescriptorPath),
      key = _fieldDescriptorPath[0],
      rest = _fieldDescriptorPath.slice(1);

  var child = map.get(key);

  if (_immutable2.default.List.isList(child)) {
    return child.reduce(function (updatedData, instance, index) {
      return updatedData.setIn([key, index], spreadDefaultValue(value, rest, instance));
    }, map);
  }

  return map.set(key, spreadDefaultValue(value, rest, child));
};

/**
 * Set default values in record data.
 */
var applyDefaults = exports.applyDefaults = function applyDefaults(fieldDescriptor, data) {
  return (0, _configHelpers.getDefaults)(fieldDescriptor).reduce(function (updatedData, defaultDescriptor) {
    return spreadDefaultValue(defaultDescriptor.value, defaultDescriptor.path, updatedData);
  }, data);
};

/**
 * Create a skeletal data record for a given CollectionSpace service.
 */
var createRecordData = exports.createRecordData = function createRecordData(recordTypeConfig) {
  return applyDefaults(recordTypeConfig.fields, createBlankRecord(recordTypeConfig));
};

/**
 * Clear uncloneable fields from record data. Existing (not undefined) values in fields that are
 * not cloneable are set to the default value if one exists, or undefined otherwise.
 */
var clearUncloneable = exports.clearUncloneable = function clearUncloneable(fieldDescriptor, data) {
  if (!fieldDescriptor) {
    return data;
  }

  if (typeof data !== 'undefined' && !(0, _configHelpers.isFieldCloneable)(fieldDescriptor)) {
    // If the field has been configured as not cloneable and there is an existing value, replace
    // the existing value with the default value if there is one, or undefined otherwise. The old
    // UI did not set uncloneable fields to the default value, but I think this was an oversight.

    return (0, _configHelpers.getDefaultValue)(fieldDescriptor);
  }

  if (_immutable2.default.Map.isMap(data)) {
    return data.reduce(function (updatedData, child, name) {
      return updatedData.set(name, clearUncloneable(fieldDescriptor[name], child));
    }, data);
  }

  if (_immutable2.default.List.isList(data)) {
    return data.reduce(function (updatedData, child, index) {
      return updatedData.set(index, clearUncloneable(fieldDescriptor, child));
    }, data);
  }

  return data;
};

/**
 * Create a new record as a clone of a given record.
 */
var cloneRecordData = exports.cloneRecordData = function cloneRecordData(recordTypeConfig, data) {
  if (!data) {
    return data;
  }

  var clone = data;

  // Delete parts that should not exist in new records.

  clone = clone.deleteIn(['document', NS_PREFIX + ':collectionspace_core']);
  clone = clone.deleteIn(['document', NS_PREFIX + ':account_permission']);

  // Reset fields that are configured as not cloneable.

  clone = clearUncloneable(recordTypeConfig.fields, clone);

  return clone;
};

/**
 * Get the document from the data record.
 */
var getDocument = exports.getDocument = function getDocument(data) {
  return data.get(DOCUMENT_PROPERTY_NAME);
};

/**
 * Comparator function to sort properties that represent XML attributes and namespace declarations
 * (those that start with '@') to the top.
 */
var attributePropertiesToTop = exports.attributePropertiesToTop = function attributePropertiesToTop(propertyNameA, propertyNameB) {
  var firstCharA = propertyNameA.charAt(0);
  var firstCharB = propertyNameB.charAt(0);

  if (firstCharA === firstCharB) {
    return 0;
  }

  if (firstCharA === '@') {
    return -1;
  }

  if (firstCharB === '@') {
    return 1;
  }

  return 0;
};

/**
 * Set the XML namespace property on a document part if it is not set, using the URI defined in the
 * field configuration. This is used in the rare case that a record retrieved from the REST API did
 * not contain one of its expected parts; for example, when a schema extension is added to a record
 * type, the new part will not appear on existing records. If a field in that part was set through
 * the UI, the part will have been created, but without the namespace attribute, which is normally
 * created through the createBlankRecord function. That function would not have been called,
 * because this was not a new record. This function can be used before saving a record to ensure
 * that any missing namespace attributes get filled in.
 */
var setXmlNamespaceAttribute = exports.setXmlNamespaceAttribute = function setXmlNamespaceAttribute(partData, partName, partDescriptor) {
  var _partName$split = partName.split(':', 1),
      _partName$split2 = _slicedToArray(_partName$split, 1),
      prefix = _partName$split2[0];

  if (prefix && !partData.get('@xmlns:' + prefix)) {
    var nsUri = (0, _get2.default)(partDescriptor, [_configHelpers.configKey, 'service', 'ns']);

    if (nsUri) {
      return partData.set('@xmlns:' + prefix, nsUri);
    }
  }

  return partData;
};

/**
 * Prepare record data for POST or PUT to the CollectionSpace REST API:
 *
 * - Document parts that may be present in data retrieved from the REST API, but that should not be
 *   present in data sent to the API, are removed.
 * - In the remaining parts, properties beginning with '@', which represent XML attributes and
 *   namespace declarations, are moved to the top. This is required by the REST API in order to
 *   properly translate the payload to XML.
 */
var prepareForSending = exports.prepareForSending = function prepareForSending(data, recordTypeConfig) {
  var preparedData = data;

  // Execute the prepareForSending function configured for the record type, if any.

  var customPrepareForSending = recordTypeConfig.prepareForSending;

  if (typeof customPrepareForSending === 'function') {
    preparedData = customPrepareForSending(preparedData, recordTypeConfig);
  }

  var documentName = preparedData.keySeq().first();

  var cspaceDocument = preparedData.get(documentName);

  // Filter out parts that don't need to be sent.
  // TODO: Use field configuration to determine what should be removed.

  cspaceDocument = cspaceDocument.filter(function (value, key) {
    return key !== NS_PREFIX + ':collectionspace_core' && key !== NS_PREFIX + ':account_permission' && key !== NS_PREFIX + ':image_metadata';
  });

  // Move XML attribute and namespace declaration properties (those that start with @) to the top,
  // since the REST API requires this.

  cspaceDocument = cspaceDocument.sortBy(function (value, name) {
    return name;
  }, attributePropertiesToTop);

  // For each part, ensure XML namespace declaration properties are set, and move XML attribute and
  // namespace declaration properties to the top.

  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = cspaceDocument.keys()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var key = _step.value;

      if (key.charAt(0) !== '@') {
        var part = cspaceDocument.get(key);

        if (_immutable2.default.Map.isMap(part)) {
          part = setXmlNamespaceAttribute(part, key, (0, _get2.default)(recordTypeConfig, ['fields', documentName, key]));
          part = part.sortBy(function (value, name) {
            return name;
          }, attributePropertiesToTop);

          cspaceDocument = cspaceDocument.set(key, part);
        }
      }
    }

    // Filter out hierarchy relations that don't have both a subject and an object, since the REST
    // API will error. These will occur when hierarchy autocomplete fields are emptied, or new
    // child instances are created but not filled in.

    // TODO: Move this to a computation in the HierarchyInput.
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  var relations = cspaceDocument.getIn(['rel:relations-common-list', 'relation-list-item']);

  if (relations && _immutable2.default.List.isList(relations)) {
    var filteredRelations = relations.filter(function (relation) {
      return (relation.getIn(['object', 'refName']) || relation.getIn(['object', 'csid'])) && (relation.getIn(['subject', 'refName']) || relation.getIn(['subject', 'csid']));
    });

    cspaceDocument = cspaceDocument.setIn(['rel:relations-common-list', 'relation-list-item'], filteredRelations);
  }

  preparedData = preparedData.set(documentName, cspaceDocument);

  // Set to null any subrecord csid fields that don't contain valid csids -- these are pointing to
  // new subrecords that haven't been saved.

  var subrecords = recordTypeConfig.subrecords;


  if (subrecords) {
    Object.values(subrecords).forEach(function (subrecordConfig) {
      var csidField = subrecordConfig.csidField;


      if (csidField) {
        var subrecordCsid = deepGet(preparedData, csidField);

        if (!(0, _csidHelpers.isCsid)(subrecordCsid)) {
          preparedData = deepSet(preparedData, csidField, null);
        }
      }
    });
  }

  return preparedData;
};

var getCoreFieldValue = exports.getCoreFieldValue = function getCoreFieldValue(data, fieldName) {
  if (data) {
    var corePart = getPart(data, 'collectionspace_core');

    if (corePart) {
      return corePart.get(fieldName);
    }
  }

  return undefined;
};

var getCsid = exports.getCsid = function getCsid(data) {
  if (!data) {
    return undefined;
  }

  var uri = data.getIn(['document', 'ns2:collectionspace_core', 'uri']);

  return uri ? uri.substring(uri.lastIndexOf('/') + 1) : undefined;
};

var getRefName = exports.getRefName = function getRefName(data) {
  if (!data) {
    return undefined;
  }

  return data.getIn(['document', 'ns2:collectionspace_core', 'refName']);
};

var getUpdatedTimestamp = exports.getUpdatedTimestamp = function getUpdatedTimestamp(data) {
  var updatedAt = getCoreFieldValue(data, 'updatedAt');

  if (!updatedAt && data) {
    // Weird records like roles have updatedAt as a child of the root node.

    var doc = data.first();

    if (doc) {
      updatedAt = doc.get('updatedAt');
    }
  }

  return updatedAt;
};

var getUpdatedUser = exports.getUpdatedUser = function getUpdatedUser(data) {
  return getCoreFieldValue(data, 'updatedBy');
};

var getCreatedTimestamp = exports.getCreatedTimestamp = function getCreatedTimestamp(data) {
  var createdAt = getCoreFieldValue(data, 'createdAt');

  if (!createdAt) {
    // Weird records like roles have updatedAt as a child of the root node.

    var doc = data.first();

    if (doc) {
      createdAt = doc.get('createdAt');
    }
  }

  return createdAt;
};

var getCreatedUser = exports.getCreatedUser = function getCreatedUser(data) {
  return getCoreFieldValue(data, 'createdBy');
};

var intPattern = /^-?\d+$/;
var floatPattern = /^-?\d+(\.\d+)?$/;
var datePattern = /^\d{4}-\d{2}-\d{2}(T00:00:00.000Z)?$/;
var dateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;

var dataTypeValidators = {
  DATA_TYPE_MAP: function DATA_TYPE_MAP(value) {
    return _immutable2.default.Map.isMap(value);
  },
  DATA_TYPE_STRING: function DATA_TYPE_STRING() {
    return true;
  },
  DATA_TYPE_INT: function DATA_TYPE_INT(value) {
    return intPattern.test(value);
  },
  DATA_TYPE_FLOAT: function DATA_TYPE_FLOAT(value) {
    return floatPattern.test(value);
  },
  DATA_TYPE_BOOL: function DATA_TYPE_BOOL(value) {
    return typeof value === 'boolean';
  },
  DATA_TYPE_DATE: function DATA_TYPE_DATE(value) {
    return datePattern.test(value);
  },
  DATA_TYPE_DATETIME: function DATA_TYPE_DATETIME(value) {
    return dateTimePattern.test(value);
  }
};

var validateDataType = function validateDataType(value, dataType) {
  var validator = dataTypeValidators[dataType];

  return validator ? validator(value) : true;
};

var doValidate = function doValidate(data) {
  var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
  var recordData = arguments[2];
  var fieldDescriptor = arguments[3];
  var expandRepeating = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;

  if (!fieldDescriptor) {
    return null;
  }

  var results = [];

  if (expandRepeating && (0, _configHelpers.isFieldRepeating)(fieldDescriptor)) {
    // This is a repeating field, and the expand flag is true. Validate each instance against the
    // current field descriptor.

    var instances = _immutable2.default.List.isList(data) ? data : _immutable2.default.List.of(data);

    instances.forEach(function (instance, index) {
      var instanceData = instances.get(index);
      var instancePath = [].concat(_toConsumableArray(path), [index]);

      var instanceResults = doValidate(instanceData, instancePath, recordData, fieldDescriptor, false);

      if (instanceResults) {
        Array.prototype.push.apply(results, instanceResults);
      }
    });

    return results.length > 0 ? results : null;
  }

  var dataType = (0, _configHelpers.getFieldDataType)(fieldDescriptor);

  if (dataType === 'DATA_TYPE_MAP' && _immutable2.default.Map.isMap(data)) {
    // Validate this field's children, and add any child results to the results array.

    var childKeys = Object.keys(fieldDescriptor).filter(function (key) {
      return key !== _configHelpers.configKey;
    });

    childKeys.forEach(function (childKey) {
      var childData = data ? data.get(childKey) : undefined;
      var childPath = [].concat(_toConsumableArray(path), [childKey]);
      var childFieldDescriptor = fieldDescriptor[childKey];

      var childResults = doValidate(childData, childPath, recordData, childFieldDescriptor);

      if (childResults) {
        Array.prototype.push.apply(results, childResults);
      }
    });
  }

  var result = void 0;

  // Check required.

  var required = (0, _configHelpers.isFieldRequired)(fieldDescriptor, recordData);

  // TODO: Does this make sense for compound fields?

  if (required && (typeof data === 'undefined' || data === null || data === '')) {
    result = {
      path: path,
      error: {
        code: _errorCodes.ERR_MISSING_REQ_FIELD,
        message: (0, _configHelpers.getRequiredMessage)(fieldDescriptor)
      }
    };
  }

  if (!result && typeof data !== 'undefined' && data !== null && data !== '') {
    // Check data type.

    if (!validateDataType(data, dataType)) {
      result = {
        path: path,
        error: {
          dataType: dataType,
          code: _errorCodes.ERR_DATA_TYPE,
          value: data
        }
      };
    }
  }

  if (!result) {
    // Custom validation.

    var customValidator = (0, _configHelpers.getFieldCustomValidator)(fieldDescriptor);

    if (customValidator) {
      var error = customValidator(data, path, recordData, fieldDescriptor);

      if (error) {
        result = {
          path: path,
          error: error
        };
      }
    }
  }

  if (result) {
    results.push(result);
  }

  return results.length > 0 ? results : null;
};

var validateField = exports.validateField = function validateField(data, path, recordData, fieldDescriptor, expandRepeating) {
  var validationResults = doValidate(data, path, recordData, fieldDescriptor, expandRepeating);

  if (validationResults) {
    // Validation results may either contain error objects, or promises that will resolve to error
    // objects (when the validation function was async). Wait for all of the promises to resolve.

    return Promise.all(validationResults.map(function (result) {
      return result.error;
    })).then(function (resolvedErrors) {
      // Convert the resolved error array into a tree of errors.

      var errorTree = _immutable2.default.Map();

      resolvedErrors.forEach(function (error, index) {
        if (error) {
          var errorPath = [].concat(_toConsumableArray(validationResults[index].path), [ERROR_KEY]);

          errorTree = deepSet(errorTree, errorPath, _immutable2.default.Map(error));
        }
      });

      return Promise.resolve(errorTree.size > 0 ? errorTree : null);
    }).catch(function () {
      // Something went wrong in an async validator. Set an error on the document.

      var errorTree = _immutable2.default.fromJS({
        document: _defineProperty({}, ERROR_KEY, {
          code: _errorCodes.ERR_UNABLE_TO_VALIDATE
        })
      });

      return Promise.resolve(errorTree);
    });
  }

  return Promise.resolve(null);
};

var validateRecordData = exports.validateRecordData = function validateRecordData(data, recordTypeConfig) {
  return validateField(data, [], data, (0, _get2.default)(recordTypeConfig, 'fields'));
};

var doCompute = function doCompute(data) {
  var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
  var recordData = arguments[2];
  var fieldDescriptor = arguments[3];
  var expandRepeating = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;

  if (!fieldDescriptor) {
    return undefined;
  }

  var results = [];

  if (expandRepeating && (0, _configHelpers.isFieldRepeating)(fieldDescriptor)) {
    // This is a repeating field, and the expand flag is true. Compute each instance.

    var instances = _immutable2.default.List.isList(data) ? data : _immutable2.default.List.of(data);

    instances.forEach(function (instance, index) {
      var instanceData = instances.get(index);
      var instancePath = [].concat(_toConsumableArray(path), [index]);

      var instanceResults = doCompute(instanceData, instancePath, recordData, fieldDescriptor, false);

      if (instanceResults) {
        Array.prototype.push.apply(results, instanceResults);
      }
    });

    return results.length > 0 ? results : undefined;
  }

  var dataType = (0, _configHelpers.getFieldDataType)(fieldDescriptor);

  if (dataType === 'DATA_TYPE_MAP' && _immutable2.default.Map.isMap(data)) {
    // Compute this field's children, and add any child results to the results array.

    var childKeys = Object.keys(fieldDescriptor).filter(function (key) {
      return key !== _configHelpers.configKey;
    });

    childKeys.forEach(function (childKey) {
      var childData = data ? data.get(childKey) : undefined;
      var childPath = [].concat(_toConsumableArray(path), [childKey]);
      var childFieldDescriptor = fieldDescriptor[childKey];

      var childResults = doCompute(childData, childPath, recordData, childFieldDescriptor);

      if (childResults) {
        Array.prototype.push.apply(results, childResults);
      }
    });
  }

  var result = void 0;

  var computer = (0, _configHelpers.getFieldComputer)(fieldDescriptor);

  if (computer) {
    var value = void 0;

    try {
      value = computer(data, path, recordData, fieldDescriptor);
    } catch (error) {
      value = Promise.reject(error);
    }

    if (typeof value !== 'undefined') {
      result = {
        path: path,
        value: value
      };
    }
  }

  if (result) {
    results.push(result);
  }

  return results.length > 0 ? results : undefined;
};

var computeField = exports.computeField = function computeField(data, path, recordData, fieldDescriptor) {
  var expandRepeating = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;

  var computationResults = doCompute(data, path, recordData, fieldDescriptor, expandRepeating);

  if (typeof computationResults !== 'undefined') {
    // Computation results may either contain values, or promises that will resolve to values (when
    // the computation function was async). Wait for all of the promises to resolve.

    return Promise.all(computationResults.map(function (result) {
      return result.value;
    })).then(function (resolvedValues) {
      // Convert the resolved value array into a tree of values.

      var valueTree = _immutable2.default.Map();

      resolvedValues.forEach(function (value, index) {
        if (value) {
          var valuePath = computationResults[index].path;

          if (valuePath && valuePath.length > 0 && expandRepeating) {
            valueTree = deepSet(valueTree, valuePath, value);
          } else {
            valueTree = value;
          }
        }
      });

      return Promise.resolve(valueTree);
    })
    // Don't catch rejections, just let the caller handle them.
    ;
  }

  return Promise.resolve(undefined);
};

var computeRecordData = exports.computeRecordData = function computeRecordData(data, recordTypeConfig) {
  return computeField(data, [], data, (0, _get2.default)(recordTypeConfig, 'fields'));
};

var isExistingRecord = exports.isExistingRecord = function isExistingRecord(data) {
  return !!(
  // TODO: Move this into record type config.

  data && (data.getIn(['document', 'ns2:collectionspace_core', 'uri']) || data.getIn(['ns2:role', '@csid']) || data.getIn(['ns3:accounts_common', '@csid'])));
};

var isNewRecord = exports.isNewRecord = function isNewRecord(data) {
  return !isExistingRecord(data);
};

var getWorkflowState = exports.getWorkflowState = function getWorkflowState(data) {
  return data ? data.getIn(['document', 'ns2:collectionspace_core', 'workflowState']) : undefined;
};

var isRecordDeprecated = exports.isRecordDeprecated = function isRecordDeprecated(data) {
  return (0, _workflowStateHelpers.isDeprecated)(getWorkflowState(data));
};

var isRecordLocked = exports.isRecordLocked = function isRecordLocked(data) {
  return (0, _workflowStateHelpers.isLocked)(getWorkflowState(data));
};

var isRecordReplicated = exports.isRecordReplicated = function isRecordReplicated(data) {
  return (0, _workflowStateHelpers.isReplicated)(getWorkflowState(data));
};

var isSecurityRecordImmutable = exports.isSecurityRecordImmutable = function isSecurityRecordImmutable(data) {
  // Accounts and roles have the concept of "immutability", which is basically the same as
  // locked.

  if (data) {
    var doc = data.first();

    return doc && (doc.get('permsProtection') === 'immutable' || doc.get('rolesProtection') === 'immutable');
  }

  return false;
};

var isRecordImmutable = exports.isRecordImmutable = function isRecordImmutable(data) {
  return isRecordLocked(data) || isRecordDeprecated(data) || isRecordReplicated(data) || isSecurityRecordImmutable(data);
};

var hasHierarchyRelations = exports.hasHierarchyRelations = function hasHierarchyRelations(data) {
  var totalItems = data.getIn(['document', 'rel:relations-common-list', 'totalItems']);

  return !!totalItems && parseInt(totalItems, 10) > 0;
};