Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.91k stars 3.83k forks source link

Validation of required field in non-required sub-document fails if the document not exists #13375

Closed ystreibel closed 1 year ago

ystreibel commented 1 year ago

Prerequisites

Mongoose version

7.1.0

Node.js version

16.13.1

MongoDB server version

7.0

Typescript version (if applicable)

No response

Description

Document schema has an array of sub-documents, for example in CollectSchema, steps.inputMethod.KeyboardSetting. This sub-document in array item is not required but its properties are.

Since 7.1.0, I have a validation error.

var CollectSchema = new Schema({
                                   _id: { type: mongoose.Types.UUID, default: uuidv4 },
                                   num: {type: Number, unique: true, required: true},
                                   name: {type: String, required: true},
                                   comment: {type: String},
                                   language: { type:  mongoose.Types.UUID, ref: 'Language', required: true},
                                   countryCode: {type: String, required: true},
                                   created: {type: Date, required: true},
                                   creationBy: { type:  mongoose.Types.UUID, ref: 'User', required: true },
                                   updateDate: {type: Date, required: true},
                                   enabled: {type: Boolean, required: true},
                                   scriptersGoal: {type: Number, required: true},
                                   scriptersInksGoal: {type: Number, required: true},
                                   inksGoal: {type: Number, required: true},
                                   steps: { type: [{
                                           priority: {type: Number, required: true},
                                           inputMethod: { type: {
                                               name: {type: String, enum: ["SI", "FUZZY", "SMOOTH", "HWR FUZZY", "HWR SMOOTH", "TEXT"], required: true},
                                               style: {type: String, enum: ["ISOLATED", "CURSIVE", "HANDPRINT", "MIXED"], required: true},
                                               keyboard: { type:  mongoose.Types.UUID, ref: 'Keyboard'},
                                               keyboardSettings: {
                                                   hideAlt: {type: Boolean, required: true},
                                                   hideCaps: {type: Boolean, required: true},
                                                   visualFeedback: {type: {
                                                           global: {type: Boolean, required: true},
                                                           keys: {type: Boolean, required: true}
                                                       }, required: true},
                                                   theme: {type: {
                                                           completeBackgroundColor: {type: String, required:true},
                                                           suggestBarBackgroundColor: {type: String, required:true},
                                                           pressKeyBackgroundColor: {type: String, required:true},
                                                           suggestBarSeparatorColor: {type: String, required:true},
                                                           alternateKeysColor: {type: String, required:true},
                                                           toggleButtonColor: {type: String, required:true},
                                                           toolBarColor: {type: String, required:true},
                                                           oolCandidateColor: {type: String, required:true},
                                                           lastCandidateColor: {type: String, required:true},
                                                           keysColor: {type: String, required:true},
                                                           inkColor: {type: String, required:true},
                                                           bestCandidateColor: {type: String, required:true},
                                                           sendKeyColor: {type: String, required:true},
                                                           enlargePressKeyViewColor: {type: String, required:true}
                                                       }, required: true}
                                               },
                                               canvasSettings: {
                                                   strokesFadeout: {type: Boolean, required: true},
                                                   pointerType: {type: String, enum: ["touch", "pen"], required: true},
                                                   backgroundColor: {type: String, pattern: "/^rgba\\((\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d*(?:\\.\\d+)?)\\)$/",required: true},
                                                   size: {
                                                       type: {
                                                           width: {type: Number},
                                                           height: {type: Number}
                                                       }, required: true
                                                   },
                                                   penWidth: {type: String, required: true},
                                                   penColor: {type: String, required: true},
                                                   drawStrokes: {type: Boolean, required: true},
                                                   snake: {type: Boolean, required: true},
                                                   center: {type: Boolean, required: true},
                                                   formats: {
                                                       type: [{
                                                           channels: {
                                                               type: [{
                                                                   name: { type: String, required: true },
                                                                   type: { type: String, required: true }
                                                               }], required: true
                                                           }
                                                       }]
                                                   },
                                                   items: {type: [
                                                       {
                                                           format: { type: Number, required: true },
                                                           itemType: { type: String, required: true},
                                                           timestamp: { type: Date, required: true },
                                                           parallelogram: {type: [Number], required: true},
                                                           lines: {type: [Number]} ,
                                                           slices: {type: [Number]},
                                                           alternates: { type: [{
                                                                   label: {type: String, required: true},
                                                                   probability: {type: Number, required: true}
                                                               }] }
                                                       }
                                                   ]}
                                               }
                                           }, required: true},
                                           writingTool: {type: String, enum: ["touch", "pen"], required: true},
                                           labelsSettings: {
                                               type: {type: String, enum: ["ALNU", "ALP", "ANA", "ARI", "CHA", "CHK", "CHUN", "DFT", "DIA", "DIG", "EMO", "EQU", "FNUM", "GPW", "HWR", "LGS", "LGW", "MAT", "MIX", "MSG", "MUS", "NEW", "NOT", "NTXT", "NUM", "SCO", "SHA", "SMS", "STS", "STW", "SYL", "SYM", "T2D", "TXT", "TXTL", "UKH", "UKS", "UNK", "VCAL", "VLGW", "VSTS", "VTX", "VWOR", "WOR"], required: true},
                                               case: {type: String, enum: ["LOW", "MIX", "NOC", "TDC", "UND", "UPP"], required: true},
                                               dataFormat: { type:  mongoose.Types.UUID, ref: 'DataFormat', required: true},
                                               language: { type:  mongoose.Types.UUID, ref: 'Language', required: true},
                                               fileSha256Id: {type: String},
                                               sort: {type: {
                                                       labelProperty: { type: String, required: true},
                                                       sortValue: { type: Number, required: true}
                                                   }, required: true},
                                               split: {type: {
                                                       criteria: {type: String, enum: ["CHA", "WOR", "STS", "NONE"], required: true},
                                                       legend: { type: [{
                                                               name: {type: String, required: true},
                                                               color: {type: String, required: true}
                                                           }]}
                                                   }, required: true},
                                               display: {type: {
                                                       labelProperty: { type: String, required: true}
                                                   }, required: true}
                                           }
                                       }], required: true}
                               },
                               {
                                   id: false
                               });
{
  "errors": {
    "steps.0.inputMethod.keyboardSettings.theme": {
      "name": "ValidatorError",
      "message": "Path `keyboardSettings.theme` is required.",
      "properties": {
        "message": "Path `keyboardSettings.theme` is required.",
        "type": "required",
        "path": "keyboardSettings.theme"
      },
      "kind": "required",
      "path": "keyboardSettings.theme"
    },
    "steps.0.inputMethod.keyboardSettings.visualFeedback": {
      "name": "ValidatorError",
      "message": "Path `keyboardSettings.visualFeedback` is required.",
      "properties": {
        "message": "Path `keyboardSettings.visualFeedback` is required.",
        "type": "required",
        "path": "keyboardSettings.visualFeedback"
      },
      "kind": "required",
      "path": "keyboardSettings.visualFeedback"
    },
    "steps.0.inputMethod.keyboardSettings.hideCaps": {
      "name": "ValidatorError",
      "message": "Path `keyboardSettings.hideCaps` is required.",
      "properties": {
        "message": "Path `keyboardSettings.hideCaps` is required.",
        "type": "required",
        "path": "keyboardSettings.hideCaps"
      },
      "kind": "required",
      "path": "keyboardSettings.hideCaps"
    },
    "steps.0.inputMethod.keyboardSettings.hideAlt": {
      "name": "ValidatorError",
      "message": "Path `keyboardSettings.hideAlt` is required.",
      "properties": {
        "message": "Path `keyboardSettings.hideAlt` is required.",
        "type": "required",
        "path": "keyboardSettings.hideAlt"
      },
      "kind": "required",
      "path": "keyboardSettings.hideAlt"
    }
  },
  "_message": "Collect validation failed",
  "name": "ValidationError",
  "message": "Collect validation failed: steps.0.inputMethod.keyboardSettings.theme: Path `keyboardSettings.theme` is required., steps.0.inputMethod.keyboardSettings.visualFeedback: Path `keyboardSettings.visualFeedback` is required., steps.0.inputMethod.keyboardSettings.hideCaps: Path `keyboardSettings.hideCaps` is required., steps.0.inputMethod.keyboardSettings.hideAlt: Path `keyboardSettings.hideAlt` is required."
}

Capture d’écran 2023-05-03 à 16 41 40

Steps to Reproduce

Create a Schema with an array of non required sub-documents within required properties. Try to save() a document.

Expected Behavior

I expect to not have required validation error for non required sub-document within required properties.

IslandRhythms commented 1 year ago

image I'm a bit confused by what you mean because your require steps in your schema

IslandRhythms commented 1 year ago

Simplified the schema so its easier to look at.

const mongoose = require('mongoose');

const { Schema } = mongoose;

var CollectSchema = new Schema({
    name: String,
    steps: { type: [{
            inputMethod: { type: {
                name: String,
                keyboardSettings: {
                    hideAlt: {type: Boolean, required: true},
                    hideCaps: {type: Boolean, required: true},
                    visualFeedback: {type: {
                            global: {type: Boolean, required: true},
                            keys: {type: Boolean, required: true}
                        }, required: true},
                    theme: {type: {
                            completeBackgroundColor: {type: String, required:true},
                            suggestBarBackgroundColor: {type: String, required:true},
                            pressKeyBackgroundColor: {type: String, required:true},
                            suggestBarSeparatorColor: {type: String, required:true},
                            alternateKeysColor: {type: String, required:true},
                            toggleButtonColor: {type: String, required:true},
                            toolBarColor: {type: String, required:true},
                            oolCandidateColor: {type: String, required:true},
                            lastCandidateColor: {type: String, required:true},
                            keysColor: {type: String, required:true},
                            inkColor: {type: String, required:true},
                            bestCandidateColor: {type: String, required:true},
                            sendKeyColor: {type: String, required:true},
                            enlargePressKeyViewColor: {type: String, required:true}
                        }, required: true}
                },
            }, required: true},
        }], required: true}
},
{
    id: false
});

const Test = mongoose.model('Test', CollectSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  await Test.create({
    name: 'Test Doc',
    steps: [{
        inputMethod: {
          name: 'Test Subdoc', 
        }
      }]
  });
}

run();
vkarpov15 commented 1 year ago

This is expected behavior, see subdocuments vs nested paths in our docs. Replace keyboardSettings: { ... } with keyboardSettings: { type: { ... } }

var CollectSchema = new Schema({
    name: String,
    steps: { type: [{
            inputMethod: { type: {
                name: String,
                keyboardSettings: {type: {
                    hideAlt: {type: Boolean, required: true},
                    hideCaps: {type: Boolean, required: true},
                    visualFeedback: {type: {
                            global: {type: Boolean, required: true},
                            keys: {type: Boolean, required: true}
                        }, required: true},
                    theme: {type: {
                            completeBackgroundColor: {type: String, required:true},
                            suggestBarBackgroundColor: {type: String, required:true},
                            pressKeyBackgroundColor: {type: String, required:true},
                            suggestBarSeparatorColor: {type: String, required:true},
                            alternateKeysColor: {type: String, required:true},
                            toggleButtonColor: {type: String, required:true},
                            toolBarColor: {type: String, required:true},
                            oolCandidateColor: {type: String, required:true},
                            lastCandidateColor: {type: String, required:true},
                            keysColor: {type: String, required:true},
                            inkColor: {type: String, required:true},
                            bestCandidateColor: {type: String, required:true},
                            sendKeyColor: {type: String, required:true},
                            enlargePressKeyViewColor: {type: String, required:true}
                        }, required: true}
                } },
            }, required: true},
        }], required: true}
},
{
    id: false
});
ystreibel commented 1 year ago

Sorry, but that is not the expected behavior? keyboardSettings should not be required, but if it's declare, sub-documents values should!

vkarpov15 commented 1 year ago

If you define your schema as follows, then keyboardSettings will not be required, but the subdocument properties will be required if keyboardSettings is set

keyboardSettings: {
  type: {
    hideAlt: {type: Boolean, required: true},
    hideCaps: {type: Boolean, required: true},
    // ...
  },
  required: false
}