Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -2808,13 +2808,14 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate

// Optimization: if primitive path with no validators, or array of primitives
// with no validators, skip validating this path entirely.
if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray) {
if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray && _pathType.instance !== 'Union') {
paths.delete(path);
} else if (_pathType.$isMongooseArray &&
!_pathType.$isMongooseDocumentArray && // Skip document arrays...
!_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays
_pathType.validators.length === 0 && // and arrays with top-level validators
_pathType.$embeddedSchemaType.validators.length === 0) {
_pathType.$embeddedSchemaType.validators.length === 0 &&
_pathType.$embeddedSchemaType.instance !== 'Union') {
paths.delete(path);
}
}
Expand Down
151 changes: 151 additions & 0 deletions lib/schema/union.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ class Union extends SchemaType {
throw new Error('Union schema type requires an array of types');
}
this.schemaTypes = options.of.map(obj => options.parentSchema.interpretAsType(key, obj, schemaOptions));

this.validators.push({
validator: () => true,
type: 'union'
});
}

cast(val, doc, init, prev, options) {
let firstValue = firstValueSymbol;
let lastError;
let bestMatch = null;
let bestMatchScore = -1;

const isObject = val != null && typeof val === 'object' && !Array.isArray(val);

// Loop through each schema type in the union. If one of the schematypes returns a value that is `=== val`, then
// use `val`. Otherwise, if one of the schematypes casted successfully, use the first successfully casted value.
// Finally, if none of the schematypes casted successfully, throw the error from the last schema type in the union.
Expand All @@ -36,13 +46,30 @@ class Union extends SchemaType {
if (casted === val) {
return casted;
}

if (isObject && casted != null && typeof casted === 'object') {
const inputKeys = Object.keys(val);
const preservedFields = inputKeys.filter(key => key in casted && key !== '_id').length;
const score = preservedFields;

if (score > bestMatchScore) {
bestMatchScore = score;
bestMatch = casted;
}
}

if (firstValue === firstValueSymbol) {
firstValue = casted;
}
} catch (error) {
lastError = error;
}
}

if (bestMatch !== null) {
return bestMatch;
}

if (firstValue !== firstValueSymbol) {
return firstValue;
}
Expand All @@ -53,6 +80,11 @@ class Union extends SchemaType {
applySetters(val, doc, init, prev, options) {
let firstValue = firstValueSymbol;
let lastError;
let bestMatch = null;
let bestMatchScore = -1;

const isObject = val != null && typeof val === 'object' && !Array.isArray(val);

// Loop through each schema type in the union. If one of the schematypes returns a value that is `=== val`, then
// use `val`. Otherwise, if one of the schematypes casted successfully, use the first successfully casted value.
// Finally, if none of the schematypes casted successfully, throw the error from the last schema type in the union.
Expand All @@ -69,13 +101,30 @@ class Union extends SchemaType {
if (castedVal === val) {
return castedVal;
}

if (isObject && castedVal != null && typeof castedVal === 'object') {
const inputKeys = Object.keys(val);
const preservedFields = inputKeys.filter(key => key in castedVal && key !== '_id').length;
const score = preservedFields;

if (score > bestMatchScore) {
bestMatchScore = score;
bestMatch = castedVal;
}
}

if (firstValue === firstValueSymbol) {
firstValue = castedVal;
}
} catch (error) {
lastError = error;
}
}

if (bestMatch !== null) {
return bestMatch;
}

if (firstValue !== firstValueSymbol) {
return firstValue;
}
Expand All @@ -88,6 +137,108 @@ class Union extends SchemaType {
schematype.schemaTypes = this.schemaTypes.map(schemaType => schemaType.clone());
return schematype;
}

/**
* Validates the value against all schema types in the union.
* The value must successfully validate against at least one schema type.
*
* @api private
*/
doValidate(value, fn, scope, options) {
if (options && options.skipSchemaValidators) {
return fn(null);
}

SchemaType.prototype.doValidate.call(this, value, function(error) {
if (error) {
return fn(error);
}
if (value == null) {
return fn(null);
}

if (value && value.schema && value.$__) {
const subdocSchema = value.schema;
for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];
if (schemaType.schema && schemaType.schema === subdocSchema) {
return schemaType.doValidate(value, fn, scope, options);
}
}
}
const validationErrors = [];
let callbackCalled = false;
let completed = 0;

for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];

schemaType.doValidate(value, (err) => {
if (callbackCalled) {
return;
}

completed++;

if (!err) {
callbackCalled = true;
return fn(null);
}

validationErrors.push(err);

if (completed === this.schemaTypes.length) {
callbackCalled = true;
return fn(validationErrors[0]);
}
}, scope, options);
}
}.bind(this), scope, options);
}

/**
* Synchronously validates the value against all schema types in the union.
* The value must successfully validate against at least one schema type.
*
* @api private
*/
doValidateSync(value, scope, options) {
if (!options || !options.skipSchemaValidators) {
const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, value, scope);
if (schemaTypeError) {
return schemaTypeError;
}
}

if (value == null) {
return;
}

if (value && value.schema && value.$__) {
const subdocSchema = value.schema;
for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];
if (schemaType.schema && schemaType.schema === subdocSchema) {
return schemaType.doValidateSync(value, scope, options);
}
}
}

const validationErrors = [];

for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];

const err = schemaType.doValidateSync(value, scope, options);
if (!err) {
return null;
}

validationErrors.push(err);
}

return validationErrors[0];
}
}

/**
Expand Down
Loading