Skip to content
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

ValidationError on setting null on subdocument field after several other values being set #14952

Closed
2 tasks done
ValeriyMaslenikov opened this issue Oct 10, 2024 · 0 comments · Fixed by #14963
Closed
2 tasks done
Milestone

Comments

@ValeriyMaslenikov
Copy link

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

8.7.1

Node.js version

21.6.2

MongoDB server version

8.0.0

Typescript version (if applicable)

5.4.5

Description

Fully working reproducible example
import mongoose from "mongoose";
const { Schema, model } = mongoose;

async function run() {
  await mongoose.connect("mongodb://localhost:27017/mongoose-test", {});

  mongoose.set("debug", true);

  // Define the nested schema
  const currentMilestoneSchema = new Schema(
    {
      id: { type: String, required: true },
    },
    {
      _id: false,
    }
  );

  const milestoneSchema = new Schema(
    {
      current: {
        type: currentMilestoneSchema,
        required: true,
      },
    },
    {
      _id: false,
    }
  );

  const campaignSchema = new Schema(
    {
      milestones: {
        type: milestoneSchema,
        required: false,
      },
    },
    {
      _id: false,
    }
  );

  const questSchema = new Schema(
    {
      campaign: { type: campaignSchema, required: false },
    },
    {
      _id: false,
    }
  );

  const parentSchema = new Schema({
    quests: [questSchema],
  });

  const ParentModel = model("Parent", parentSchema);

  // Create and save the parent document
  let doc = new ParentModel({
    quests: [
      {
        campaign: {
          milestones: {
            current: {
              id: "milestone1",
            },
          },
        },
      },
    ],
  });

  await doc.save();

  // Set the nested schema to null
  doc.quests[0].campaign.milestones.current = {
    id: "milestone1",
  };
  doc.quests[0].campaign.milestones.current = {
    id: "",
  };

  doc.quests[0].campaign.milestones = null;

  try {
    // Save the document again
    await doc.save();
  } catch (err) {
    // Expected validation error
    console.error("Validation error:", err);
  }

  await mongoose.disconnect();
}

run().catch((err) => console.error(err));

The following example leads into the following output:

Validation error because of `current.id` is required
Mongoose: parents.insertOne({ quests: [ { campaign: { milestones: { current: { id: 'milestone1' } } } } ], _id: ObjectId("6707efc27c06131efbea4e40"), __v: 0}, {})
Validation error: Error: Parent validation failed: quests.0.campaign.milestones.current.id: Path `campaign.milestones.current.id` is required., quests.0.campaign.milestones.current: Path `campaign.milestones.current` is required.
    at ValidationError.inspect (/private/tmp/mongoose-bug-1/node_modules/mongoose/lib/error/validation.js:52:26)
    at formatValue (node:internal/util/inspect:805:19)
    at inspect (node:internal/util/inspect:364:10)
    at formatWithOptionsInternal (node:internal/util/inspect:2298:40)
    at formatWithOptions (node:internal/util/inspect:2160:10)
    at console.value (node:internal/console/constructor:351:14)
    at console.warn (node:internal/console/constructor:384:61)
    at run (/private/tmp/mongoose-bug-1/second.mts:90:13)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  errors: {
    'quests.0.campaign.milestones.current.id': ValidatorError: Path `campaign.milestones.current.id` is required.
        at validate (/private/tmp/mongoose-bug-1/node_modules/mongoose/lib/schemaType.js:1385:13)
        at SchemaType.doValidate (/private/tmp/mongoose-bug-1/node_modules/mongoose/lib/schemaType.js:1369:7)
        at /private/tmp/mongoose-bug-1/node_modules/mongoose/lib/document.js:3071:18
        at process.processTicksAndRejections (node:internal/process/task_queues:77:11) {
      properties: [Object],
      kind: 'required',
      path: 'campaign.milestones.current.id',
      value: null,
      reason: undefined,
      [Symbol(mongoose#validatorError)]: true
    },
    'quests.0.campaign.milestones.current': ValidatorError: Path `campaign.milestones.current` is required.
        at validate (/private/tmp/mongoose-bug-1/node_modules/mongoose/lib/schemaType.js:1385:13)
        at SchemaType.doValidate (/private/tmp/mongoose-bug-1/node_modules/mongoose/lib/schemaType.js:1369:7)
        at SchemaSubdocument.doValidate (/private/tmp/mongoose-bug-1/node_modules/mongoose/lib/schema/subdocument.js:265:35)
        at /private/tmp/mongoose-bug-1/node_modules/mongoose/lib/document.js:3071:18
        at process.processTicksAndRejections (node:internal/process/task_queues:77:11) {
      properties: [Object],
      kind: 'required',
      path: 'campaign.milestones.current',
      value: null,
      reason: undefined,
      [Symbol(mongoose#validatorError)]: true
    }
  },
  _message: 'Parent validation failed'
}

Whereas If I change the required to false on nested subdocs:

  const currentMilestoneSchema = new Schema(
    {
-      id: { type: String, required: true },
+      id: { type: String, required: false },
    },
    {
      _id: false,
    }
  );

  const milestoneSchema = new Schema(
    {
      current: {
        type: currentMilestoneSchema,
-       required: true,
+       required: false,
      },
    },
    {
      _id: false,
    }
  );

then mongoose generates pretty absurd query, which tries to set both subdocuments and their parents to null:

Absurd output
Mongoose: parents.insertOne({ quests: [ { campaign: { milestones: { current: { id: 'milestone1' } } } } ], _id: ObjectId("6707f10577d2fec90b937034"), __v: 0}, {})
Mongoose: parents.updateOne({ _id: ObjectId("6707f10577d2fec90b937034"), __v: 0 }, { '$set': { 'quests.0.campaign.milestones.current.id': null, 'quests.0.campaign.milestones.current': null, 'quests.0.campaign.milestones': null }}, {})
Validation error: MongoServerError: Updating the path 'quests.0.campaign.milestones.current' would create a conflict at 'quests.0.campaign.milestones.current'
    at UpdateOneOperation.execute (/private/tmp/mongoose-bug-1/node_modules/mongodb/src/operations/update.ts:151:32)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at tryOperation (/private/tmp/mongoose-bug-1/node_modules/mongodb/src/operations/execute_operation.ts:263:14)
    at executeOperation (/private/tmp/mongoose-bug-1/node_modules/mongodb/src/operations/execute_operation.ts:109:12)
    at Collection.updateOne (/private/tmp/mongoose-bug-1/node_modules/mongodb/src/collection.ts:362:12) {
  errorResponse: {
    index: 0,
    code: 40,
    errmsg: "Updating the path 'quests.0.campaign.milestones.current' would create a conflict at 'quests.0.campaign.milestones.current'"
  },
  index: 0,
  code: 40,
  [Symbol(errorLabels)]: Set(0) {}
}

But if you go and stop making two sets before setting to null, e.g.

  doc.quests[0].campaign.milestones.current = {
    id: "milestone1",
  };

-  doc.quests[0].campaign.milestones.current = {
-    id: "milestone2",
-  };

  doc.quests[0].campaign.milestones = null;

Then output looks as expected:

Mongoose: parents.insertOne({ quests: [ { campaign: { milestones: { current: { id: 'milestone1' } } } } ], _id: ObjectId("6707f1f01235b106373ed7d2"), __v: 0}, {})
Mongoose: parents.updateOne({ _id: ObjectId("6707f1f01235b106373ed7d2"), __v: 0 }, { '$set': { 'quests.0.campaign.milestones': null } }, {})

Steps to Reproduce

Copy-paste the first code block from description.
Use the following package.json contents:

{
  "name": "mongoose-bug-1",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "mongoose": "^8.7.1",
    "typescript": "^5.6.3"
  },
  "devDependencies": {
    "@tsconfig/node21": "^21.0.3",
    "tsx": "^4.19.1"
  }
}

Execute it via:

npx tsx ./index.mts

Expected Behavior

I'd expect that I can change the value of field which contains subdocument as many times I need, and after that set null within a single business transaction without validation errors on save.

@vkarpov15 vkarpov15 added this to the 8.7.2 milestone Oct 14, 2024
vkarpov15 added a commit that referenced this issue Oct 17, 2024
fix(document): recursively clear modified subpaths when setting deeply nested subdoc to null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants