ember-changeset
Version:
163 lines (151 loc) • 5.79 kB
JavaScript
import { isArrayObject, objectToArray, arrayToObject, isChange, getChangeValue, normalizeObject } from 'validated-changeset';
function isMergeableObject(value) {
return isNonNullObject(value) && !isSpecial(value);
}
function isNonNullObject(value) {
return !!value && typeof value === 'object' && value !== null;
}
function isSpecial(value) {
let stringValue = Object.prototype.toString.call(value);
return stringValue === '[object RegExp]' || stringValue === '[object Date]';
}
// Reconsider when enumerable symbols are removed - https://github.com/emberjs/ember.js/commit/ef0e277533b3eab01e58d68b79d7e37d8b11ee34
// function getEnumerableOwnPropertySymbols(target) {
// return Object.getOwnPropertySymbols
// ? Object.getOwnPropertySymbols(target).filter(symbol => {
// return Object.prototype.propertyIsEnumerable.call(target, symbol)
// })
// : [];
// }
function getKeys(target) {
return Object.keys(target);
// .concat(getEnumerableOwnPropertySymbols(target))
}
function propertyIsOnObject(object, property) {
try {
return property in object;
} catch {
return false;
}
}
// Ember Data models don't respond as expected to foo.hasOwnProperty, so we do a special check
function hasEmberDataProperty(target, key, options) {
let fields = options.safeGet(target, 'constructor.fields');
return fields instanceof Map && fields.has(key);
}
// Protects from prototype poisoning and unexpected merging up the prototype chain.
function propertyIsUnsafe(target, key, options) {
if (hasEmberDataProperty(target, key, options)) {
return false;
}
return propertyIsOnObject(target, key) &&
// Properties are safe to merge if they don't exist in the target yet,
!(Object.prototype.hasOwnProperty.call(target, key) && Object.prototype.propertyIsEnumerable.call(target, key)
// unsafe if they exist up the prototype chain,
); // and also unsafe if they're nonenumerable.
}
/**
* DFS - traverse depth first until find object with `value`. Then go back up tree and try on next key
* Need to exhaust all possible avenues.
*
* @method buildPathToValue
*/
function buildPathToValue(source, options, kv, possibleKeys) {
Object.keys(source).forEach(key => {
let possible = source[key];
if (possible && isChange(possible)) {
kv[[...possibleKeys, key].join('.')] = getChangeValue(possible);
return;
}
if (possible && typeof possible === 'object') {
buildPathToValue(possible, options, kv, [...possibleKeys, key]);
}
});
return kv;
}
/**
* `source` will always have a leaf key `value` with the property we want to set
*
* @method mergeTargetAndSource
*/
function mergeTargetAndSource(target, source, options) {
getKeys(source).forEach(key => {
// proto poisoning. So can set by nested key path 'person.name'
if (propertyIsUnsafe(target, key, options)) {
// if safeSet, we will find keys leading up to value and set
if (options.safeSet) {
const kv = buildPathToValue(source, options, {}, []);
// each key will be a path nested to the value `person.name.other`
if (Object.keys(kv).length > 0) {
// we found some keys!
for (key in kv) {
const val = kv[key];
options.safeSet(target, key, val);
}
}
}
return;
}
// else safe key on object
if (propertyIsOnObject(target, key) && isMergeableObject(source[key]) && !isChange(source[key])) {
options.safeSet(target, key, mergeDeep(options.safeGet(target, key), options.safeGet(source, key), options));
} else {
let next = source[key];
if (isChange(next)) {
return options.safeSet(target, key, getChangeValue(next));
}
// if just some normal leaf value, then set
return options.safeSet(target, key, normalizeObject(next));
}
});
return target;
}
/**
* goal is to mutate target with source's properties, ensuring we dont encounter
* pitfalls of { ..., ... } spread syntax overwriting keys on objects that we merged
*
* This is also adjusted for Ember peculiarities. Specifically `options.safeSet` will allows us
* to handle properties on Proxy objects (that aren't the target's own property)
*
* @method mergeDeep
*/
function mergeDeep(target, source, options = {}) {
options.safeGet = options.safeGet || function (obj, key) {
return obj[key];
};
options.safeSet = options.safeSet || function (obj, key, value) {
return obj[key] = value;
};
let sourceIsArray = Array.isArray(source);
let targetIsArray = Array.isArray(target);
let sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
if (!sourceAndTargetTypesMatch) {
let sourceIsArrayLike = isArrayObject(source);
if (targetIsArray && sourceIsArrayLike) {
return objectToArray(mergeTargetAndSource(arrayToObject(target), source, options));
}
return source;
} else if (sourceIsArray) {
return source;
} else if (target === null || target === undefined) {
/**
* If the target was set to null or undefined, we always want to return the source.
* There is nothing to merge.
*
* Without this explicit check, typeof null === typeof {any object-like thing}
* which means that mergeTargetAndSource will be called, and you can't merge with null
*/
return source;
} else {
try {
return mergeTargetAndSource(target, source, options);
} catch (e) {
// this is very unlikely to be hit but lets throw an error otherwise
throw new Error('Unable to `mergeDeep` with your data. Are you trying to merge two ember-data objects? Please file an issue with ember-changeset.', {
cause: e
});
}
}
}
export { mergeDeep as default };
//# sourceMappingURL=merge-deep.js.map