-
-
Notifications
You must be signed in to change notification settings - Fork 4k
fix: add validation support for Union schema types #15734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
59c420a
1116f3b
5d2f052
974c1ef
5343cdc
d0bfe63
df32490
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ const SchemaUnionOptions = require('../options/schemaUnionOptions'); | |
| const SchemaType = require('../schemaType'); | ||
|
|
||
| const firstValueSymbol = Symbol('firstValue'); | ||
| const castSchemaTypeSymbol = Symbol('mongoose#castSchemaType'); | ||
|
|
||
| /*! | ||
| * ignore | ||
|
|
@@ -20,11 +21,18 @@ 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 firstSchemaType = null; | ||
| let lastError; | ||
|
|
||
| // 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. | ||
|
|
@@ -34,16 +42,26 @@ class Union extends SchemaType { | |
| try { | ||
| const casted = this.schemaTypes[i].cast(val, doc, init, prev, options); | ||
| if (casted === val) { | ||
| if (casted != null && typeof casted === 'object' && casted.$__ != null) { | ||
| casted.$__[castSchemaTypeSymbol] = this.schemaTypes[i]; | ||
| } | ||
| return casted; | ||
| } | ||
|
|
||
| if (firstValue === firstValueSymbol) { | ||
| firstValue = casted; | ||
| firstSchemaType = this.schemaTypes[i]; | ||
| } | ||
| } catch (error) { | ||
| lastError = error; | ||
| } | ||
| } | ||
|
|
||
| if (firstValue !== firstValueSymbol) { | ||
| // Store which schema type was used for this cast | ||
| if (firstValue != null && typeof firstValue === 'object' && firstValue.$__ != null) { | ||
| firstValue.$__[castSchemaTypeSymbol] = firstSchemaType; | ||
| } | ||
| return firstValue; | ||
| } | ||
| throw lastError; | ||
|
|
@@ -52,7 +70,9 @@ class Union extends SchemaType { | |
| // Setters also need to be aware of casting - we need to apply the setters of the entry in the union we choose. | ||
| applySetters(val, doc, init, prev, options) { | ||
| let firstValue = firstValueSymbol; | ||
| let firstSchemaType = null; | ||
| let lastError; | ||
|
|
||
| // 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. | ||
|
|
@@ -67,16 +87,25 @@ class Union extends SchemaType { | |
| castedVal = this.schemaTypes[i].cast(castedVal, doc, init, prev, options); | ||
| } | ||
| if (castedVal === val) { | ||
| if (castedVal != null && typeof castedVal === 'object' && castedVal.$__ != null) { | ||
| castedVal.$__[castSchemaTypeSymbol] = this.schemaTypes[i]; | ||
| } | ||
| return castedVal; | ||
| } | ||
|
|
||
| if (firstValue === firstValueSymbol) { | ||
| firstValue = castedVal; | ||
| firstSchemaType = this.schemaTypes[i]; | ||
| } | ||
| } catch (error) { | ||
| lastError = error; | ||
| } | ||
| } | ||
|
|
||
| if (firstValue !== firstValueSymbol) { | ||
| if (firstValue != null && typeof firstValue === 'object' && firstValue.$__ != null) { | ||
| firstValue.$__[castSchemaTypeSymbol] = firstSchemaType; | ||
| } | ||
| return firstValue; | ||
| } | ||
| throw lastError; | ||
|
|
@@ -88,6 +117,132 @@ 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); | ||
| } | ||
|
|
||
| // Check if we stored which schema type was used during casting | ||
| if (value && value.$__ && value.$__[castSchemaTypeSymbol]) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also don't like relying on |
||
| const schemaType = value.$__[castSchemaTypeSymbol]; | ||
| return schemaType.doValidate(value, fn, scope, options); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // For primitives, we need to determine which schema type accepts the value by attempting to cast. | ||
| // We can't store metadata on primitives, so we re-cast to find the matching schema type. | ||
| let matchedSchemaType = null; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't like re-casting as part of validation. For one thing, while casting is almost always going to be deterministic, there's no guarantee that casting the casted value will give you the same result back. This also breaks the convention that casting and validation are separate. |
||
| for (let i = 0; i < this.schemaTypes.length; ++i) { | ||
| try { | ||
| const casted = this.schemaTypes[i].cast(value, scope, false, null, options); | ||
| if (casted === value) { | ||
| // This schema type accepts the value as-is | ||
| matchedSchemaType = this.schemaTypes[i]; | ||
| break; | ||
| } | ||
| if (matchedSchemaType == null) { | ||
| // First schema type that successfully casts the value | ||
| matchedSchemaType = this.schemaTypes[i]; | ||
| } | ||
| } catch (error) { | ||
| // This schema type can't cast the value, try the next one | ||
| } | ||
| } | ||
|
|
||
| if (matchedSchemaType) { | ||
| return matchedSchemaType.doValidate(value, fn, scope, options); | ||
| } | ||
|
|
||
| // If no schema type can cast the value, return an error | ||
| return fn(new Error(`Value ${value} does not match any schema type in union`)); | ||
| }.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; | ||
| } | ||
|
|
||
| // Check if we stored which schema type was used during casting (for subdocuments) | ||
| if (value && value.$__ && value.$__[castSchemaTypeSymbol]) { | ||
| const schemaType = value.$__[castSchemaTypeSymbol]; | ||
| return schemaType.doValidateSync(value, scope, options); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // For primitives, we need to determine which schema type accepts the value by attempting to cast. | ||
| // We can't store metadata on primitives, so we re-cast to find the matching schema type. | ||
| let matchedSchemaType = null; | ||
| for (let i = 0; i < this.schemaTypes.length; ++i) { | ||
| try { | ||
| const casted = this.schemaTypes[i].cast(value, scope, false, null, options); | ||
| if (casted === value) { | ||
| // This schema type accepts the value as-is | ||
| matchedSchemaType = this.schemaTypes[i]; | ||
| break; | ||
| } | ||
| if (matchedSchemaType == null) { | ||
| // First schema type that successfully casts the value | ||
| matchedSchemaType = this.schemaTypes[i]; | ||
| } | ||
| } catch (error) { | ||
| // This schema type can't cast the value, try the next one | ||
| } | ||
| } | ||
|
|
||
| if (matchedSchemaType) { | ||
| return matchedSchemaType.doValidateSync(value, scope, options); | ||
| } | ||
|
|
||
| // If no schema type can cast the value, return an error | ||
| return new Error(`Value ${value} does not match any schema type in union`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I presume this is so Mongoose doesn't skip unions when getting paths to validate? If so, I'd prefer to add an explicit exception to avoid unions instead of adding a fake validator.