diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 398019b2..288a5564 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -17,6 +17,7 @@ var defineScope = require('./scope.js').defineScope; var g = require('strong-globalize')(); var mergeQuery = utils.mergeQuery; var idEquals = utils.idEquals; +var idsHaveDuplicates = utils.idsHaveDuplicates; var ModelBaseClass = require('./model.js'); var applyFilter = require('./connectors/memory').applyFilter; var ValidationError = require('./validations.js').ValidationError; @@ -2502,10 +2503,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelToRef, param modelFrom.validate(propertyName, function(err) { var embeddedList = this[propertyName] || []; var ids = embeddedList.map(function(m) { return m[idName] && m[idName].toString(); }); // mongodb - var uniqueIds = ids.filter(function(id, pos) { - return utils.findIndexOf(ids, id, idEquals) === pos; - }); - if (ids.length !== uniqueIds.length) { + if (idsHaveDuplicates(ids)) { this.errors.add(propertyName, 'contains duplicate `' + idName + '`', 'uniqueness'); err(false); } @@ -3155,10 +3153,7 @@ RelationDefinition.referencesMany = function referencesMany(modelFrom, modelToRe modelFrom.validate(relationName, function(err) { var ids = this[fk] || []; - var uniqueIds = ids.filter(function(id, pos) { - return utils.findIndexOf(ids, id, idEquals) === pos; - }); - if (ids.length !== uniqueIds.length) { + if (idsHaveDuplicates(ids)) { var msg = 'contains duplicate `' + modelTo.modelName + '` instance'; this.errors.add(relationName, msg, 'uniqueness'); err(false); diff --git a/lib/utils.js b/lib/utils.js index dcd9760b..bb6f638e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,6 +26,7 @@ exports.findIndexOf = findIndexOf; exports.collectTargetIds = collectTargetIds; exports.idName = idName; exports.rankArrayElements = rankArrayElements; +exports.idsHaveDuplicates = idsHaveDuplicates; var g = require('strong-globalize')(); var traverse = require('traverse'); @@ -685,3 +686,48 @@ function collectTargetIds(targetData, idPropertyName) { function idName(m) { return m.definition.idName() || 'id'; } + +/** + * Check a list of IDs to see if there are any duplicates. + * + * @param {Array} The array of IDs to check + * @returns {boolean} If any duplicates were found + */ +function idsHaveDuplicates(ids) { + // use Set if available and all ids are of string or number type + var hasDuplicates = undefined; + var i, j; + if (typeof Set === 'function') { + var uniqueIds = new Set(); + for (i = 0; i < ids.length; ++i) { + var idType = typeof ids[i]; + if (idType === 'string' || idType === 'number') { + if (uniqueIds.has(ids[i])) { + hasDuplicates = true; + break; + } else { + uniqueIds.add(ids[i]); + } + } else { + // ids are not all string/number that can be checked via Set, stop and do the slow test + break; + } + } + if (hasDuplicates === undefined && uniqueIds.length === ids.length) { + hasDuplicates = false; + } + } + if (hasDuplicates === undefined) { + // fast check was inconclusive or unavailable, do the slow check + // can still optimize this by doing 1/2 N^2 instead of the full N^2 + for (i = 0; i < ids.length && hasDuplicates === undefined; ++i) { + for (j = 0; j < i; ++j) { + if (idEquals(ids[i], ids[j])) { + hasDuplicates = true; + break; + } + } + } + } + return hasDuplicates === true; +}