array-sync
Version:
Data synchronisation module for Node.js
293 lines (261 loc) • 10.9 kB
JavaScript
/**
* Determine if something is an array.
* @param {String} valueType A string returned from the `type` function.
* @return {Boolean} Return `true` if the object is an array.
*/
const isArray = (valueType) => valueType === '[object Array]';
/**
* Determine if something is a function.
* @param {String} valueType A string returned from the `type` function.
* @return {Boolean} Return `true` if the object is a function.
*/
const isFunction = (valueType) => valueType === '[object Function]';
/**
* Determine if something is a symbol.
* @param {String} valueType A string returned from the `type` symbol.
* @return {Boolean} Return `true` if the object is a symbol.
*/
const isSymbol = (valueType) => valueType === '[object Symbol]';
/**
* Determine if something is a plain object.
* @param {String} valueType A string returned from the `type` function.
* @return {Boolean} Return `true` if the object is a plain object.
*/
const isObject = (valueType) => valueType === '[object Object]';
/**
* Determine if something is a plain object, or an array.
* @param {String} valueType A string returned from the `type` function.
* @return {Boolean} Return `true` if the object is a plain object, or an array.
*/
const isObjectOrArray = (valueType) =>
isObject(valueType) || isArray(valueType);
/**
* Convert the type of an object to string for easy comparison.
* @param {Any} obj An object to determine the value of.
* @return {Boolean} Return a string representing the type of object (i.e. `[object Object]` or `[object Array]`).
*/
const type = (obj) => Object.prototype.toString.call(obj);
/**
* Used to compare if two items are equal.
* @param {Any} objOne The first object to compare.
* @param {Any} objTwo Compare the first object to this object.
* @return {Boolean} Return `true` if the object is the same, otherwise return `false`.
*/
const isEqual = (objOne, objTwo) => {
// Get the value type
const objOneType = type(objOne);
// Compare properties
if (isArray(objOneType)) {
return objOne.every((v, i) => comparator(v, objTwo[i]));
}
return Object.getOwnPropertyNames(objOne).every((key) =>
comparator(objOne[key], objTwo[key])
);
};
/**
* The default comparator function, which will loop through arrays to compare them.
* @param {Any} objOne The first object to compare.
* @param {Any} objTwo Compare the first object to this object.
* @return {Boolean} Return `true` if the object is the same, otherwise return `false`.
*/
const comparator = (objOne, objTwo) => {
// Get the object type
const objOneType = type(objOne);
// If an object or array, compare recursively
if (isObjectOrArray(objOneType)) {
return isEqual(objOne, objTwo);
}
// If it's a function, convert to a string and compare.
if (isFunction(objOneType) || isSymbol(objOneType)) {
return objOne.toString() === objTwo.toString();
}
// Otherwise, just compare.
return objOne === objTwo;
};
/**
* Takes an `Array` of objects, and a `key` that exists within the objects and returns an array
* containing the value of the key on each object within the array.
* @param {Array} a An array of objects.
* @param {String} key A key that exists on each object within the array.
* @return {Array} An array of values pertaining to the value of the key on each object.
*/
const mapToKey = (a, key) => a.map((val) => val[key]);
/**
* Find anything that was in the `source` array but does not exist in the `update` array.
* @param {Array} source Source array.
* @param {Array} update An updated version of the source array.
* @param {Object} opts An Object containing information to alter the outcome of the function.
* @return {Array} An array of items that are in the `source` array but don't exist
* in the `update` array.
*/
const findMissingValues = (source, update, opts) =>
source.filter(function(sourceValue) {
return (
update.find(function(element, index, array) {
// If we have a key, we only want to compare the value of the keys.
return opts.key
? (opts.comparator || comparator)(
sourceValue[opts.key],
element[opts.key]
) === true
: (opts.comparator || comparator)(sourceValue, element) ===
true;
}) === undefined
);
});
/**
* Find anything that is new in the `update` array.
* @param {Array} source Source array.
* @param {Array} update An updated version of the source array.
* @param {Object} opts An Object containing information to alter the outcome of the function.
* @return {Array} An array of items that are in the `update` array but don't exist
* in the `source` array.
*/
const findNewValues = (source, update, opts) =>
update.filter(function(updateValue) {
return (
source.find(function(element, index, array) {
// If we have a key, we don't want to create.
return opts.key
? (opts.comparator || comparator)(
updateValue[opts.key],
element[opts.key]
) === true
: (opts.comparator || comparator)(updateValue, element) ===
true;
}) === undefined
);
});
/**
* Find anything that is exactly the same between the `source` array and the `update` array.
* @param {Array} source Source array.
* @param {Array} removedCreatedChanged An updated version of the source array.
* @param {Object} opts An Object containing information to alter the outcome of the function.
* @return {Array} An array of items that appear in the `update` array and exactly match
* their counterpart in the `source` array.
*/
const findUnchangedValues = (source, removedCreatedChanged, opts) =>
source.filter(function(sourceValue) {
return (
removedCreatedChanged.find(function(element, index, array) {
// If we have a key, we only want to compare the actual key is the same.
if (opts.key) {
return (
(opts.comparator || comparator)(
sourceValue[opts.key],
element[opts.key]
) === true
);
}
return (
(opts.comparator || comparator)(sourceValue, element) ===
true
);
}) === undefined
);
});
/**
* Find anything that has changed between the `source` array and the `update` array.
* @param {Array} source The source array
* @param {Array} update An updated version of the source array.
* @param {Object} opts An Object containing information to alter the outcome of the function.
* @return {Array} An array of items that appear in the `update` array and do not match
* their counterpart in the `source` array.
*/
const findChangedValues = (source, update, opts) =>
source
.filter(function(sourceValue) {
return (
update.find(function(element, index, array) {
// If we have a key, we only want to compare when the key is the same.
return (
(opts.comparator || comparator)(
sourceValue[opts.key],
element[opts.key]
) === true &&
(opts.comparator || comparator)(
sourceValue,
element,
opts.key
) !== true
);
}) !== undefined
);
// We always have a key if this function is executing, make sure we pass back the changed values, not those from the source.
})
.map(function(sourceValue) {
return update.find(function(element, index, array) {
return (
(opts.comparator || comparator)(
sourceValue[opts.key],
element[opts.key]
) === true
);
});
});
/**
* Data synchronization module for Node.js.
*
* @param {Array} source Source array.
* @param {Array} update An updated version of the source array.
* @param {Object} opts An object of options.
* @return {Promise} A promise that will be resolved or rejected with the result.
*/
module.exports = function arraySync(source, update, opts = {}) {
if (!source) {
throw Error(
'You must provide a source Array for arraySync to inspect.'
);
}
if (!update) {
throw Error(
'You must provide an update Array for arraySync to inspect.'
);
}
if (opts.comparator && !opts.key) {
throw Error(
'You must provide a key when passing in a custom comparator function.'
);
}
if (opts.key && typeof opts.keyOnly === 'undefined') {
opts.keyOnly = true;
}
// Default return object.
const r = {
get create() {
console.log(
'DeprecationWarning: The create property has been deprecated, use the created property instead.'
);
return this.removed;
},
get remove() {
console.log(
'DeprecationWarning: The remove property has been deprecated, use the removed property instead.'
);
return this.removed;
},
removed: [],
unchanged: [],
created: []
};
// Find the missing values.
r.removed = findMissingValues(source, update, opts);
// Find the new values.
r.created = findNewValues(source, update, opts);
// Add support for a more complex evaluation of Objects, if the `opts.key` has been provided.
if (opts.key) {
r.changed = findChangedValues(source, update, opts);
}
// Determine the unchanged values (those that aren't new, nor missing).
r.unchanged = findUnchangedValues(
source,
r.removed.concat(r.created, r.changed || []),
opts
);
// If we have a `key`, transform the results to contain only the key Object.
if (opts.key && opts.keyOnly) {
r.removed = mapToKey(r.removed, opts.key);
r.unchanged = mapToKey(r.unchanged, opts.key);
}
return r;
};