2017-03-23 16:27:59 +09:00
|
|
|
|
import Users from 'meteor/vulcan:users';
|
2018-06-28 12:31:35 +02:00
|
|
|
|
import merge from 'lodash/merge';
|
|
|
|
|
import find from 'lodash/find';
|
2018-06-29 13:06:52 +02:00
|
|
|
|
import isPlainObject from 'lodash/isPlainObject';
|
2018-06-29 13:04:35 +02:00
|
|
|
|
import set from 'lodash/set';
|
2018-06-29 13:06:52 +02:00
|
|
|
|
import size from 'lodash/size';
|
2018-06-29 13:04:35 +02:00
|
|
|
|
|
|
|
|
|
import { removePrefix, filterPathsByPrefix } from './path_utils';
|
2016-11-27 19:12:54 +09:00
|
|
|
|
|
2016-03-30 19:08:06 +09:00
|
|
|
|
// add support for nested properties
|
2018-03-08 11:28:29 +09:00
|
|
|
|
export const deepValue = function(obj, path){
|
2016-11-15 09:02:30 +01:00
|
|
|
|
const pathArray = path.split('.');
|
|
|
|
|
|
|
|
|
|
for (var i=0; i < pathArray.length; i++) {
|
2016-11-15 10:17:27 +01:00
|
|
|
|
obj = obj[pathArray[i]];
|
2016-11-15 09:02:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-03-30 19:08:06 +09:00
|
|
|
|
return obj;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// see http://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects
|
2018-03-08 11:28:29 +09:00
|
|
|
|
export const flatten = function(data) {
|
2016-03-30 19:08:06 +09:00
|
|
|
|
var result = {};
|
|
|
|
|
function recurse (cur, prop) {
|
|
|
|
|
|
2018-09-12 11:59:00 +09:00
|
|
|
|
if (Object.prototype.toString.call(cur) !== '[object Object]') {
|
2016-03-30 19:08:06 +09:00
|
|
|
|
result[prop] = cur;
|
|
|
|
|
} else if (Array.isArray(cur)) {
|
|
|
|
|
for(var i=0, l=cur.length; i<l; i++)
|
2018-09-12 11:59:00 +09:00
|
|
|
|
recurse(cur[i], prop + '[' + i + ']');
|
2016-03-30 19:08:06 +09:00
|
|
|
|
if (l == 0)
|
|
|
|
|
result[prop] = [];
|
|
|
|
|
} else {
|
|
|
|
|
var isEmpty = true;
|
|
|
|
|
for (var p in cur) {
|
|
|
|
|
isEmpty = false;
|
2018-09-12 11:59:00 +09:00
|
|
|
|
recurse(cur[p], prop ? prop+'.'+p : p);
|
2016-03-30 19:08:06 +09:00
|
|
|
|
}
|
|
|
|
|
if (isEmpty && prop)
|
|
|
|
|
result[prop] = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-12 11:59:00 +09:00
|
|
|
|
recurse(data, '');
|
2016-03-30 19:08:06 +09:00
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-13 16:05:46 +09:00
|
|
|
|
/**
|
|
|
|
|
* @method Mongo.Collection.getInsertableFields
|
|
|
|
|
* Get an array of all fields editable by a specific user for a given collection
|
|
|
|
|
* @param {Object} user – the user for which to check field permissions
|
|
|
|
|
*/
|
2018-03-08 11:28:29 +09:00
|
|
|
|
export const getInsertableFields = function (schema, user) {
|
2016-06-13 16:05:46 +09:00
|
|
|
|
const fields = _.filter(_.keys(schema), function (fieldName) {
|
|
|
|
|
var field = schema[fieldName];
|
2018-06-21 14:27:20 +02:00
|
|
|
|
return Users.canCreateField(user, field);
|
2016-06-13 16:05:46 +09:00
|
|
|
|
});
|
|
|
|
|
return fields;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @method Mongo.Collection.getEditableFields
|
2016-11-23 17:22:29 +09:00
|
|
|
|
* Get an array of all fields editable by a specific user for a given collection (and optionally document)
|
2016-06-13 16:05:46 +09:00
|
|
|
|
* @param {Object} user – the user for which to check field permissions
|
|
|
|
|
*/
|
2018-03-08 11:28:29 +09:00
|
|
|
|
export const getEditableFields = function (schema, user, document) {
|
2016-06-13 16:05:46 +09:00
|
|
|
|
const fields = _.filter(_.keys(schema), function (fieldName) {
|
|
|
|
|
var field = schema[fieldName];
|
2018-06-21 14:27:20 +02:00
|
|
|
|
return Users.canUpdateField(user, field, document);
|
2016-06-13 16:05:46 +09:00
|
|
|
|
});
|
|
|
|
|
return fields;
|
|
|
|
|
};
|
|
|
|
|
|
2018-08-06 10:29:36 +09:00
|
|
|
|
export const isEmptyValue = value => (typeof value === 'undefined' || value === null || value === '' || Array.isArray(value) && value.length === 0);
|
2018-06-28 12:31:35 +02:00
|
|
|
|
|
2018-06-29 13:06:52 +02:00
|
|
|
|
/**
|
|
|
|
|
* Merges values. It takes into account the current, original and deleted values,
|
|
|
|
|
* and the merge produces the proper type for simple objects or arrays.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} props
|
|
|
|
|
* Form component props. Only specific properties for this function are documented.
|
|
|
|
|
* @param {*} props.currentValue
|
|
|
|
|
* Current value of the field
|
|
|
|
|
* @param {*} props.documentValue
|
|
|
|
|
* Original value of the field
|
|
|
|
|
* @return {*|undefined}
|
2018-06-29 20:02:20 +02:00
|
|
|
|
* Merged value or undefined if no merge was performed
|
2018-06-29 13:06:52 +02:00
|
|
|
|
*/
|
2018-06-28 12:31:35 +02:00
|
|
|
|
export const mergeValue = ({
|
|
|
|
|
currentValue,
|
|
|
|
|
documentValue,
|
2018-06-29 13:06:52 +02:00
|
|
|
|
deletedValues: deletedFields,
|
2018-06-29 20:02:20 +02:00
|
|
|
|
path,
|
2018-06-29 13:06:52 +02:00
|
|
|
|
locale,
|
2018-06-28 12:31:35 +02:00
|
|
|
|
datatype,
|
|
|
|
|
}) => {
|
|
|
|
|
if (locale) {
|
|
|
|
|
// note: intl fields are of type Object but should be treated as Strings
|
2018-06-29 20:01:55 +02:00
|
|
|
|
return currentValue || documentValue || '';
|
2018-06-29 13:06:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// note: retrieve nested deleted values is performed here to avoid skipping
|
|
|
|
|
// the merge in case the current field is not in `currentValues` but a nested
|
|
|
|
|
// property has been removed directly by path
|
2018-06-29 20:02:20 +02:00
|
|
|
|
const deletedValues = getNestedDeletedValues(path, deletedFields);
|
2018-06-29 13:06:52 +02:00
|
|
|
|
const hasDeletedValues = !!size(deletedValues);
|
|
|
|
|
if ((Array.isArray(currentValue) || hasDeletedValues) && find(datatype, ['type', Array])) {
|
|
|
|
|
return merge([], documentValue, currentValue, deletedValues);
|
|
|
|
|
} else if ((isPlainObject(currentValue) || hasDeletedValues) && find(datatype, ['type', Object])) {
|
|
|
|
|
return merge({}, documentValue, currentValue, deletedValues);
|
2018-06-28 12:31:35 +02:00
|
|
|
|
}
|
2018-06-29 12:55:55 +02:00
|
|
|
|
return undefined;
|
2018-06-28 12:31:35 +02:00
|
|
|
|
};
|
2018-06-29 13:04:35 +02:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a list of field names to an object of deleted values.
|
|
|
|
|
*
|
|
|
|
|
* @param {string[]|Object.<string|string>} deletedFields
|
|
|
|
|
* List of deleted field names or paths
|
|
|
|
|
* @param {Object|Array=} accumulator={}
|
|
|
|
|
* Value to reduce the values to
|
|
|
|
|
* @return {Object|Array}
|
|
|
|
|
* Deleted values, with the structure defined by taking the received deleted
|
|
|
|
|
* fields as paths
|
|
|
|
|
* @example
|
|
|
|
|
* const deletedFields = [
|
|
|
|
|
* 'field.subField',
|
|
|
|
|
* 'field.subFieldArray[0]',
|
|
|
|
|
* 'fieldArray[0]',
|
|
|
|
|
* 'fieldArray[2].name',
|
|
|
|
|
* ];
|
|
|
|
|
* getNestedDeletedValues(deletedFields);
|
|
|
|
|
* // => { 'field': { 'subField': null, 'subFieldArray': [null] }, 'fieldArray': [null, undefined, { name: null } }
|
|
|
|
|
*/
|
|
|
|
|
export const getDeletedValues = (deletedFields, accumulator = {}) =>
|
|
|
|
|
deletedFields.reduce(
|
|
|
|
|
(deletedValues, path) => set(deletedValues, path, null),
|
|
|
|
|
accumulator,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Filters the given field names by prefix, removes it from each one of them
|
|
|
|
|
* and convert the list to an object of deleted values.
|
|
|
|
|
*
|
|
|
|
|
* @param {string=} prefix
|
|
|
|
|
* Prefix to filter and remove from deleted fields
|
|
|
|
|
* @param {string[]|Object.<string|string>} deletedFields
|
|
|
|
|
* List of deleted field names or paths
|
|
|
|
|
* @param {Object|Array=} accumulator={}
|
|
|
|
|
* Value to reduce the values to
|
|
|
|
|
* @return {Object.<string, null>}
|
|
|
|
|
* Object keyed with the given deleted fields, valued with `null`
|
|
|
|
|
* @example
|
|
|
|
|
* const deletedFields = [
|
|
|
|
|
* 'field.subField',
|
|
|
|
|
* 'field.subFieldArray[0]',
|
|
|
|
|
* 'fieldArray[0]',
|
|
|
|
|
* 'fieldArray[2].name',
|
|
|
|
|
* ];
|
|
|
|
|
* getNestedDeletedValues('field', deletedFields);
|
|
|
|
|
* // => { 'subField': null, 'subFieldArray': [null] }
|
|
|
|
|
* getNestedDeletedValues('fieldArray', deletedFields);
|
|
|
|
|
* // => { '0': null, '2': { 'name': null } }
|
|
|
|
|
* getNestedDeletedValues('fieldArray', deletedFields, []);
|
|
|
|
|
* // => [null, undefined, { 'name': null } ]
|
|
|
|
|
*/
|
|
|
|
|
export const getNestedDeletedValues = (prefix, deletedFields, accumulator = {}) =>
|
|
|
|
|
getDeletedValues(
|
|
|
|
|
removePrefix(prefix, filterPathsByPrefix(prefix, deletedFields)),
|
|
|
|
|
accumulator,
|
|
|
|
|
);
|
2018-08-06 10:29:36 +09:00
|
|
|
|
|
|
|
|
|
export const getFieldType = datatype => datatype[0].type;
|
|
|
|
|
/**
|
|
|
|
|
* Get appropriate null value for various field types
|
|
|
|
|
*
|
|
|
|
|
* @param {Array} datatype
|
|
|
|
|
* Field's datatype property
|
|
|
|
|
*/
|
|
|
|
|
export const getNullValue = datatype => {
|
|
|
|
|
const fieldType = getFieldType(datatype);
|
|
|
|
|
if (fieldType === Array) {
|
|
|
|
|
return [];
|
|
|
|
|
} else if (fieldType === Boolean) {
|
|
|
|
|
return false;
|
|
|
|
|
} else if (fieldType === String) {
|
|
|
|
|
return '';
|
2018-08-06 10:43:00 +09:00
|
|
|
|
} else if (fieldType === Number) {
|
|
|
|
|
return '';
|
2018-08-06 10:29:36 +09:00
|
|
|
|
} else {
|
|
|
|
|
// normalize to null
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|