UNPKG

cspace-ui

Version:
813 lines (756 loc) 32.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateRecordData = exports.validateField = exports.spreadDefaultValue = exports.setXmlNamespaceAttribute = exports.prepareForSending = exports.prepareClonedHierarchy = exports.normalizeRecordData = exports.normalizeFieldValue = exports.isRecordReplicated = exports.isRecordLocked = exports.isRecordImmutable = exports.isRecordDeprecated = exports.isNewRecord = exports.isExistingRecord = exports.initializeChildren = exports.hasNarrowerHierarchyRelations = exports.hasHierarchyRelations = exports.getWorkflowState = exports.getUpdatedUser = exports.getUpdatedTimestamp = exports.getStickyFieldValues = exports.getRefName = exports.getPartPropertyName = exports.getPartNSPropertyName = exports.getPart = exports.getDocument = exports.getCsid = exports.getCreatedUser = exports.getCreatedTimestamp = exports.getCoreFieldValue = exports.getCommonFieldValue = exports.deepSet = exports.deepGet = exports.deepDelete = exports.createRecordData = exports.createBlankRecord = exports.copyValue = exports.computeRecordData = exports.computeField = exports.cloneRecordData = exports.clearUncloneable = exports.attributePropertiesToTop = exports.applyDefaults = exports.ERROR_KEY = void 0; var _immutable = _interopRequireDefault(require("immutable")); var _get = _interopRequireDefault(require("lodash/get")); var _errorCodes = require("../constants/errorCodes"); var _xmlNames = require("../constants/xmlNames"); var _configHelpers = require("./configHelpers"); var _csidHelpers = require("./csidHelpers"); var _relationListHelpers = require("./relationListHelpers"); var _workflowStateHelpers = require("./workflowStateHelpers"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const numericPattern = /^[0-9]$/; const ERROR_KEY = exports.ERROR_KEY = '[error]'; const getPartPropertyName = partName => `${_xmlNames.NS_PREFIX}:${partName}`; exports.getPartPropertyName = getPartPropertyName; const getPart = (data, partName) => data.getIn([_xmlNames.DOCUMENT_PROPERTY_NAME, getPartPropertyName(partName)]); exports.getPart = getPart; const getPartNSPropertyName = prefix => `@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. */ exports.getPartNSPropertyName = getPartNSPropertyName; const deepGet = (data, path) => { if (!Array.isArray(path) || path.length === 0) { throw new Error('path must be a non-empty array'); } if (!data) { return undefined; } const [key, ...rest] = path; let value; if ((key === '0' || key === 0) && !_immutable.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. */ exports.deepGet = deepGet; const deepSet = (data, path, value) => { if (!Array.isArray(path) || path.length === 0) { throw new Error('path must be a non-empty array'); } const [key, ...rest] = path; const isKeyNumeric = numericPattern.test(key); let normalizedData; if (data) { if (isKeyNumeric && !_immutable.default.List.isList(data)) { // Promote a single (non-list) value into a list when a numeric key is supplied. normalizedData = _immutable.default.List.of(data); } else { normalizedData = data; } } else if (isKeyNumeric) { normalizedData = _immutable.default.List(); } else { normalizedData = _immutable.default.Map(); } const 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. */ exports.deepSet = deepSet; const deepDelete = (data, path) => // First call deepSet with undefined value to ensure the path exists. Then call deleteIn. deepSet(data, path).deleteIn(path); exports.deepDelete = deepDelete; const normalizeFieldValue = (fieldDescriptor, fieldValue, expandRepeating = true) => { let normalizedValue = fieldValue; if (fieldDescriptor && typeof fieldValue !== 'undefined') { if (_immutable.default.Map.isMap(normalizedValue)) { normalizedValue = normalizedValue.map((childValue, childName) => normalizeFieldValue(fieldDescriptor[childName], childValue)); } else if (_immutable.default.List.isList(normalizedValue)) { normalizedValue = normalizedValue.map(instance => normalizeFieldValue(fieldDescriptor, instance, false)); } if (expandRepeating && (0, _configHelpers.isFieldRepeating)(fieldDescriptor) && !_immutable.default.List.isList(normalizedValue)) { normalizedValue = _immutable.default.List.of(normalizedValue); } } return normalizedValue; }; exports.normalizeFieldValue = normalizeFieldValue; const normalizeRecordData = (recordTypeConfig, data) => { let normalizedData = normalizeFieldValue(recordTypeConfig.fields, data); if (recordTypeConfig.normalizeRecordData) { normalizedData = recordTypeConfig.normalizeRecordData(data, recordTypeConfig); } return normalizedData; }; /** * Create a blank data record for a given CollectionSpace record type. */ exports.normalizeRecordData = normalizeRecordData; const createBlankRecord = recordTypeConfig => { const { fields } = recordTypeConfig; const documentKey = Object.keys(fields)[0]; const documentDescriptor = fields[documentKey]; const document = {}; // On some records, e.g. roles, the document has a namespace. const documentNsUri = (0, _get.default)(documentDescriptor, [_configHelpers.configKey, 'service', 'ns']); if (documentNsUri) { const nsPrefix = documentKey.split(':', 2)[0]; document[getPartNSPropertyName(nsPrefix)] = documentNsUri; } // Most records have parts with a namespace. const partKeys = Object.keys(documentDescriptor); partKeys.forEach(partKey => { const partDescriptor = documentDescriptor[partKey]; const nsUri = (0, _get.default)(partDescriptor, [_configHelpers.configKey, 'service', 'ns']); if (nsUri) { const nsPrefix = partKey.split(':', 2)[0]; document[partKey] = { [getPartNSPropertyName(nsPrefix)]: nsUri }; } }); return _immutable.default.fromJS({ [documentKey]: document }); }; /** * Deeply copy the value at a given path in a source record into a destination record. */ exports.createBlankRecord = createBlankRecord; const copyValue = (fieldDescriptorPath, sourceData, destData) => { if (!fieldDescriptorPath || fieldDescriptorPath.length === 0 || !_immutable.default.Map.isMap(sourceData)) { return sourceData; } const [key, ...rest] = fieldDescriptorPath; const sourceChild = sourceData.get(key); let destChild = destData.get(key); if (!_immutable.default.Map.isMap(destChild)) { destChild = _immutable.default.Map(); } if (_immutable.default.List.isList(sourceChild)) { return sourceChild.reduce((updatedDestData, instance, index) => updatedDestData.setIn([key, index], copyValue(rest, instance, destChild)), destData.set(key, _immutable.default.List())); } return destData.set(key, copyValue(rest, sourceChild, destChild)); }; /** * 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. */ exports.copyValue = copyValue; const spreadDefaultValue = (value, fieldDescriptorPath, data) => { if (!fieldDescriptorPath || fieldDescriptorPath.length === 0) { return typeof data === 'undefined' ? value : data; } let map; if (typeof data === 'undefined') { map = _immutable.default.Map(); } else if (_immutable.default.Map.isMap(data)) { map = data; } else { return data; } const [key, ...rest] = fieldDescriptorPath; const child = map.get(key); if (_immutable.default.List.isList(child)) { return child.reduce((updatedData, instance, index) => updatedData.setIn([key, index], spreadDefaultValue(value, rest, instance)), map); } return map.set(key, spreadDefaultValue(value, rest, child)); }; /** * Set default values in record data. */ exports.spreadDefaultValue = spreadDefaultValue; const applyDefaults = (fieldDescriptor, data) => (0, _configHelpers.getDefaults)(fieldDescriptor).reduce((updatedData, defaultDescriptor) => spreadDefaultValue(defaultDescriptor.value, defaultDescriptor.path, updatedData), data); /** * Initialize the child fields of a complex field. */ exports.applyDefaults = applyDefaults; const initializeChildren = (fieldDescriptor, data, value = null) => { const childKeys = Object.keys(fieldDescriptor).filter(key => key !== _configHelpers.configKey); if (childKeys.length === 0) { return data; } const map = data || _immutable.default.Map(); return childKeys.reduce((updatedMap, childKey) => { const childValue = updatedMap.get(childKey); if (typeof childValue === 'undefined') { return updatedMap.set(childKey, value); } if (_immutable.default.Map.isMap(childValue)) { return updatedMap.set(childKey, initializeChildren(fieldDescriptor[childKey], childValue, value)); } if (_immutable.default.List.isList(childValue)) { return updatedMap.set(childKey, childValue.map(childValueListItem => initializeChildren(fieldDescriptor[childKey], childValueListItem, value))); } return updatedMap; }, map); }; /** * Create a skeletal data record for a given CollectionSpace service. */ exports.initializeChildren = initializeChildren; const createRecordData = recordTypeConfig => 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. */ exports.createRecordData = createRecordData; const 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 _immutable.default.Map.isMap(data) ? applyDefaults(fieldDescriptor) : (0, _configHelpers.getDefaultValue)(fieldDescriptor); } if (_immutable.default.Map.isMap(data)) { return data.reduce((updatedData, child, name) => updatedData.set(name, clearUncloneable(fieldDescriptor[name], child)), data); } if (_immutable.default.List.isList(data)) { return data.reduce((updatedData, child, index) => updatedData.set(index, clearUncloneable(fieldDescriptor, child)), data); } return data; }; exports.clearUncloneable = clearUncloneable; const prepareClonedHierarchy = (fromCsid, data) => { // Process hierarchy following a clone. Delete children, and use the new record placeholder // csid in relations to parents. // TODO: Move this into config? let relations = data.getIn(['document', 'rel:relations-common-list', 'relation-list-item']); const updatedData = data.deleteIn(['document', 'rel:relations-common-list']); if (!relations) { return updatedData; } if (!_immutable.default.List.isList(relations)) { relations = _immutable.default.List.of(relations); } const broaderRelation = (0, _relationListHelpers.findBroaderRelation)(fromCsid, relations); if (!broaderRelation) { return updatedData; } return updatedData.setIn(['document', 'rel:relations-common-list', 'relation-list-item'], _immutable.default.List.of(broaderRelation.setIn(['subject', 'csid'], _relationListHelpers.placeholderCsid))); }; /** * Create a new record as a clone of a given record. */ exports.prepareClonedHierarchy = prepareClonedHierarchy; const cloneRecordData = (recordTypeConfig, csid, data) => { if (!data) { return data; } let clone = data; // Delete parts that should not exist in new records. clone = clone.deleteIn(['document', `${_xmlNames.NS_PREFIX}:collectionspace_core`]); clone = clone.deleteIn(['document', `${_xmlNames.NS_PREFIX}:account_permission`]); // Reset fields that are configured as not cloneable. clone = clearUncloneable(recordTypeConfig.fields, clone); clone = prepareClonedHierarchy(csid, clone); return clone; }; /** * Get the document from the data record. */ exports.cloneRecordData = cloneRecordData; const getDocument = data => data.get(_xmlNames.DOCUMENT_PROPERTY_NAME); /** * Comparator function to sort properties that represent XML attributes and namespace declarations * (those that start with '@') to the top. */ exports.getDocument = getDocument; const attributePropertiesToTop = (propertyNameA, propertyNameB) => { const firstCharA = propertyNameA.charAt(0); const 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. */ exports.attributePropertiesToTop = attributePropertiesToTop; const setXmlNamespaceAttribute = (partData, partName, partDescriptor) => { const nsUri = (0, _get.default)(partDescriptor, [_configHelpers.configKey, 'service', 'ns']); const [prefix] = partName.split(':', 1); if (prefix && nsUri) { const data = _immutable.default.Map.isMap(partData) ? partData : _immutable.default.Map(); if (!data.get(`@xmlns:${prefix}`)) { return data.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. */ exports.setXmlNamespaceAttribute = setXmlNamespaceAttribute; const prepareForSending = (data, recordTypeConfig) => { let preparedData = data; // Execute the prepareForSending function configured for the record type, if any. const customPrepareForSending = recordTypeConfig.prepareForSending; if (typeof customPrepareForSending === 'function') { preparedData = customPrepareForSending(preparedData, recordTypeConfig); } const documentName = preparedData.keySeq().first(); let 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((value, key) => key !== `${_xmlNames.NS_PREFIX}:collectionspace_core` && key !== `${_xmlNames.NS_PREFIX}:account_permission` && key !== `${_xmlNames.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((value, name) => name, attributePropertiesToTop); // For each part, ensure XML namespace declaration properties are set, and move XML attribute and // namespace declaration properties to the top. if (documentName === 'document') { cspaceDocument.keySeq().forEach(key => { if (key.charAt(0) !== '@') { let part = cspaceDocument.get(key); part = setXmlNamespaceAttribute(part, key, (0, _get.default)(recordTypeConfig, ['fields', documentName, key])); if (_immutable.default.Map.isMap(part)) { part = part.sortBy((value, name) => 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. const relations = cspaceDocument.getIn(['rel:relations-common-list', 'relation-list-item']); if (relations && _immutable.default.List.isList(relations)) { const filteredRelations = relations.filter(relation => (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. const { subrecords } = recordTypeConfig; if (subrecords) { Object.values(subrecords).forEach(subrecordConfig => { const { csidField } = subrecordConfig; if (csidField) { const subrecordCsid = deepGet(preparedData, csidField); if (!(0, _csidHelpers.isCsid)(subrecordCsid)) { preparedData = deepSet(preparedData, csidField, null); } } }); } return preparedData; }; exports.prepareForSending = prepareForSending; const getCoreFieldValue = (data, fieldName) => { if (data) { const corePart = getPart(data, 'collectionspace_core'); if (corePart) { return corePart.get(fieldName); } } return undefined; }; exports.getCoreFieldValue = getCoreFieldValue; const getCommonFieldValue = (data, fieldName) => { if (!data) { return undefined; } const document = data.get('document'); if (!document) { return undefined; } const partName = document.keySeq().find(key => key.endsWith('_common')); const commonPart = document.get(partName); return commonPart.get(fieldName); }; exports.getCommonFieldValue = getCommonFieldValue; const getCsid = data => { if (!data) { return undefined; } const uri = data.getIn(['document', 'ns2:collectionspace_core', 'uri']); return uri ? uri.substring(uri.lastIndexOf('/') + 1) : undefined; }; exports.getCsid = getCsid; const getRefName = data => { if (!data) { return undefined; } return data.getIn(['document', 'ns2:collectionspace_core', 'refName']); }; exports.getRefName = getRefName; const getUpdatedTimestamp = data => { let updatedAt = getCoreFieldValue(data, 'updatedAt'); if (!updatedAt && data) { // Weird records like roles have updatedAt as a child of the root node. const doc = data.first(); if (doc) { updatedAt = doc.get('updatedAt'); } } return updatedAt; }; exports.getUpdatedTimestamp = getUpdatedTimestamp; const getUpdatedUser = data => getCoreFieldValue(data, 'updatedBy'); exports.getUpdatedUser = getUpdatedUser; const getCreatedTimestamp = data => { let createdAt = getCoreFieldValue(data, 'createdAt'); if (!createdAt) { // Weird records like roles have updatedAt as a child of the root node. const doc = data.first(); if (doc) { createdAt = doc.get('createdAt'); } } return createdAt; }; exports.getCreatedTimestamp = getCreatedTimestamp; const getCreatedUser = data => getCoreFieldValue(data, 'createdBy'); exports.getCreatedUser = getCreatedUser; const intPattern = /^-?\d+$/; const floatPattern = /^-?(\d+(\.\d+)?|\.\d+)$/; const dateTimePattern = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z)?$/; // The pre-5.0 UI allowed non-zero times on date-typed fields, and the REST API still allows it, // so there may be data that contains non-zero times. This means the UI needs to support that, // even if the calendar picker always generates dates without times. // const datePattern = /^\d{4}-\d{2}-\d{2}(T00:00:00.000Z)?$/; const datePattern = dateTimePattern; const dataTypeValidators = { DATA_TYPE_MAP: value => _immutable.default.Map.isMap(value), DATA_TYPE_STRING: () => true, DATA_TYPE_INT: value => intPattern.test(value), DATA_TYPE_FLOAT: value => floatPattern.test(value), DATA_TYPE_BOOL: value => typeof value === 'boolean' || value === 'true' || value === 'false', DATA_TYPE_DATE: value => datePattern.test(value), DATA_TYPE_DATETIME: value => dateTimePattern.test(value) }; const validateDataType = (value, dataType) => { const validator = dataTypeValidators[dataType]; return validator ? validator(value) : true; }; const doValidate = (validationContext, expandRepeating = true) => { const { data, path = [], fieldDescriptor } = validationContext; if (!fieldDescriptor) { return null; } const 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. const instances = _immutable.default.List.isList(data) ? data : _immutable.default.List.of(data); instances.forEach((instance, index) => { const instanceResults = doValidate({ ...validationContext, data: instance, path: [...path, index] }, false); if (instanceResults) { Array.prototype.push.apply(results, instanceResults); } }); return results.length > 0 ? results : null; } const dataType = (0, _configHelpers.getFieldDataType)(fieldDescriptor); if (dataType === 'DATA_TYPE_MAP' && _immutable.default.Map.isMap(data)) { // Validate this field's children, and add any child results to the results array. const childKeys = Object.keys(fieldDescriptor).filter(key => key !== _configHelpers.configKey); childKeys.forEach(childKey => { const childResults = doValidate({ ...validationContext, data: data ? data.get(childKey) : undefined, path: [...path, childKey], fieldDescriptor: fieldDescriptor[childKey] }, true); if (childResults) { Array.prototype.push.apply(results, childResults); } }); } let result; // Check required. const required = (0, _configHelpers.isFieldRequired)(validationContext); // TODO: Does this make sense for compound fields? if (required && (typeof data === 'undefined' || data === null || data === '')) { result = { 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, error: { dataType, code: _errorCodes.ERR_DATA_TYPE, value: data } }; } } if (!result) { // Custom validation. const customValidator = (0, _configHelpers.getFieldCustomValidator)(fieldDescriptor); if (customValidator) { const error = customValidator(validationContext); if (error) { result = { path, error }; } } } if (result) { results.push(result); } return results.length > 0 ? results : null; }; const validateField = (validationContext, expandRepeating) => { const validationResults = doValidate(validationContext, 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(result => result.error)).then(resolvedErrors => { // Convert the resolved error array into a tree of errors. let errorTree = _immutable.default.Map(); resolvedErrors.forEach((error, index) => { if (error) { const errorPath = [...validationResults[index].path, ERROR_KEY]; errorTree = deepSet(errorTree, errorPath, _immutable.default.Map(error)); } }); return Promise.resolve(errorTree.size > 0 ? errorTree : null); }).catch(() => { // Something went wrong in an async validator. Set an error on the document. const errorTree = _immutable.default.fromJS({ document: { [ERROR_KEY]: { code: _errorCodes.ERR_UNABLE_TO_VALIDATE } } }); return Promise.resolve(errorTree); }); } return Promise.resolve(null); }; exports.validateField = validateField; const validateRecordData = (data, subrecordData, recordTypeConfig) => validateField({ data, path: [], recordData: data, subrecordData, fieldDescriptor: (0, _get.default)(recordTypeConfig, 'fields') }, true); exports.validateRecordData = validateRecordData; const doCompute = (computeContext, expandRepeating = true) => { const { data, path = [], fieldDescriptor } = computeContext; if (!fieldDescriptor) { return undefined; } const results = []; if (expandRepeating && (0, _configHelpers.isFieldRepeating)(fieldDescriptor)) { // This is a repeating field, and the expand flag is true. Compute each instance. const instances = _immutable.default.List.isList(data) ? data : _immutable.default.List.of(data); instances.forEach((instance, index) => { const instanceResults = doCompute({ ...computeContext, data: instance, path: [...path, index] }, false); if (instanceResults) { Array.prototype.push.apply(results, instanceResults); } }); return results.length > 0 ? results : undefined; } const dataType = (0, _configHelpers.getFieldDataType)(fieldDescriptor); if (dataType === 'DATA_TYPE_MAP' && _immutable.default.Map.isMap(data)) { // Compute this field's children, and add any child results to the results array. const childKeys = Object.keys(fieldDescriptor).filter(key => key !== _configHelpers.configKey); childKeys.forEach(childKey => { const childResults = doCompute({ ...computeContext, data: data ? data.get(childKey) : undefined, path: [...path, childKey], fieldDescriptor: fieldDescriptor[childKey] }, true); if (childResults) { Array.prototype.push.apply(results, childResults); } }); } let result; const computer = (0, _configHelpers.getFieldComputer)(fieldDescriptor); if (computer) { let value; try { value = computer(computeContext); } catch (error) { value = Promise.reject(error); } if (typeof value !== 'undefined') { result = { path, value }; } } if (result) { results.push(result); } return results.length > 0 ? results : undefined; }; const computeField = (computeContext, expandRepeating) => { const computationResults = doCompute(computeContext, 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(result => result.value)).then(resolvedValues => { // Convert the resolved value array into a tree of values. let valueTree = _immutable.default.Map(); resolvedValues.forEach((value, index) => { if (typeof value !== 'undefined') { const valuePath = computationResults[index].path; if (valuePath && valuePath.length > 0 && expandRepeating) { const prevValue = valueTree.getIn(valuePath); const nextValue = prevValue ? prevValue.mergeDeep(value) : value; valueTree = deepSet(valueTree, valuePath, nextValue); } else { valueTree = value; } } }); return Promise.resolve(valueTree); }) // Don't catch rejections, just let the caller handle them. ; } return Promise.resolve(undefined); }; exports.computeField = computeField; const computeRecordData = (data, subrecordData, recordTypeConfig) => computeField({ data, path: [], recordData: data, subrecordData, fieldDescriptor: (0, _get.default)(recordTypeConfig, 'fields') }, true); exports.computeRecordData = computeRecordData; const isExistingRecord = data => !!( // TODO: Move this into record type config. data && (data.getIn(['document', 'ns2:collectionspace_core', 'uri']) || data.getIn(['ns2:role', '@csid']) || data.getIn(['ns2:accounts_common', '@csid']))); exports.isExistingRecord = isExistingRecord; const isNewRecord = data => !isExistingRecord(data); exports.isNewRecord = isNewRecord; const getWorkflowState = data => data ? data.getIn(['document', 'ns2:collectionspace_core', 'workflowState']) : undefined; exports.getWorkflowState = getWorkflowState; const isRecordDeprecated = data => (0, _workflowStateHelpers.isDeprecated)(getWorkflowState(data)); exports.isRecordDeprecated = isRecordDeprecated; const isRecordLocked = data => (0, _workflowStateHelpers.isLocked)(getWorkflowState(data)); exports.isRecordLocked = isRecordLocked; const isRecordReplicated = data => (0, _workflowStateHelpers.isReplicated)(getWorkflowState(data)); exports.isRecordReplicated = isRecordReplicated; const isRecordImmutable = data => isRecordLocked(data) || isRecordDeprecated(data) || isRecordReplicated(data); exports.isRecordImmutable = isRecordImmutable; const hasHierarchyRelations = data => { const items = data.getIn(['document', 'rel:relations-common-list', 'relation-list-item']); return !!items && (!_immutable.default.List.isList(items) || items.size > 0); }; exports.hasHierarchyRelations = hasHierarchyRelations; const hasNarrowerHierarchyRelations = (csid, data) => { let items = data.getIn(['document', 'rel:relations-common-list', 'relation-list-item']); if (!items) { return false; } if (!_immutable.default.List.isList(items)) { items = _immutable.default.List.of(items); } return !!items.find(relation => relation.get('predicate') === 'hasBroader' && relation.getIn(['object', 'csid']) === csid); }; exports.hasNarrowerHierarchyRelations = hasNarrowerHierarchyRelations; const getStickyFieldValues = (recordTypeConfig, data) => { const stickyFields = (0, _configHelpers.getStickyFields)(recordTypeConfig.fields); const stickyData = stickyFields.reduce((updatedData, path) => copyValue(path, data, updatedData), _immutable.default.Map()); return stickyData; }; exports.getStickyFieldValues = getStickyFieldValues;