0

I have a simple Mongoose schema that looks like this:

const fruitSchema = new mongoose.Schema({
    name: {
        type: String
        required: true
    },
    type: {
        type: String,
        required: true
    }
})

schema.index({ name: 1, type: 1 }, { unique: true })
const Fruit = mongoose.model('Fruit', fruitSchema)

Under normal circumstances, the table is unique on (name, type) so the user is able to store multiple types for each fruit. However, I want to allow the user to only store one type if the name of the Fruit is apple.

So basically, I want make the following check before a save is made:

if (newFruit.name === 'apple') {
        if (Fruit.find({ name: 'apple' }).count >= 1) throw new Error()
}

One way of enforcing this is to execute the code above in a pre-save hook. But, I was just wondering if there would be an in-built way to specify this within the Mongoose schema itself?

Thanks for the help!

SOLUTION: In addition to the solution kindly provided by @SuleymanSah below, I thought I'd post what I finally ended up using.

const fruitSchema = new mongoose.Schema({
    fruit: {
      type: String
      required: true,
      async validate(name) {
        if (name === 'apple') {
            let fruit 

            try {
                fruit = await Fruit.findOne({ name })
            } catch (e) {
                console.error('[Fruit Model] An error occurred during validation.')
                console.error(e)
                throw e // rethrow error if findOne call fails since fruit will be null and this validation will pass with the next statement
            }

            if (fruit) throw new Error(`A fruit for ${name} already exists.`)
        }
        },
    type: {
      type: String,
      required: true
    }
})

schema.index({ fruit: 1, type: 1 }, { unique: true })
const Fruit = mongoose.model('Fruit', fruitSchema)
philosopher
  • 1,079
  • 2
  • 16
  • 29

3 Answers3

1

You can do it like this with express, for example when saving a new fruit:

const express = require("express");
const router = express.Router();

router.post("/save", (req, res) => {
  const { name, type  } = req.body; //this is coming from front-end

Fruit.findOne({ name }).then(fruit=> {
    if (fruit) return res.status(400).json({ name: "This fruit already exist!" });

This will prevent any fruit with same name from saving to database

Oner T.
  • 394
  • 1
  • 7
  • 22
  • Thanks. This is one way to go about it but does not enforce it on a schema level. I thought a pre-save hook would probably be better option to this because it would make sure of this requirement on the DB level. Are you suggesting it would be better practice to enforce this on the router level? – philosopher Dec 27 '19 at 06:54
  • Yes, I do like this and suggesting it because, in my opinion, this is another layer to check all the controls, pre-requirements before saving, which is a better design and Schema - as its name suggests, should consist of the schema -the plan of the models. – Oner T. Dec 27 '19 at 07:01
  • Got it. Thanks! Marking this as the answer. – philosopher Dec 27 '19 at 07:02
1

You can use custom validators like this:

const schema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  type: {
    type: String,
    required: true,
    validate: {
      validator: async function() {
        if (this.name === "apple") {
          let doc = await this.constructor.findOne({ name: "apple" });
          return Boolean(!doc);
        }
      },
      message: props => "For apple only one type can be."
    }
  }
});
SuleymanSah
  • 17,153
  • 5
  • 33
  • 54
  • Hey, this was what I was actually looking for. I have a couple of synchronous validations on the type field as well as well. How would I incorporate those as well into the code? Thanks for your help! I marked yours as the answer. – philosopher Dec 27 '19 at 08:16
  • Hi, you are welcome. I suggest you reading mongoose custom validation docs, and implement yourself, you can ask a new question if you are stuck. – SuleymanSah Dec 27 '19 at 08:19
  • I read through the docs and am using a validation function like this: http://jsfiddle.net/04o1phLy. Is there any difference between this one and the one you have given in your answer? Also I noticed you used `this.constructor.findOne` in your answer rather than `Fruit.findOne`. Any reason for that? – philosopher Dec 27 '19 at 08:28
  • inside the schema we don't have access to Fruit. So I used this.constructor to refer the actual model. – SuleymanSah Dec 27 '19 at 08:35
  • It seems to work for me. In the sense, I do have access to it. Strange. But thanks, this is the way to go about it. I am using Joi for front end validations anyway. – philosopher Dec 27 '19 at 08:51
  • Had one more quick question for you. Would the `await findOne` call within the validation need to be wrapped in a try catch block? If not, why? – philosopher Dec 27 '19 at 09:04
  • @philosopher it would definitely be a good idea, I skipped that for simplicity. – SuleymanSah Dec 27 '19 at 09:05
0

i think this is similar query you need to add index and set it to unique restrict to store duplicate values in mongodb

  • Well, I have already defined a unique compound index on (name, type). Problem is that it is supposed to unique on name, only for a certain subset of the rows. – philosopher Dec 27 '19 at 06:58
  • That wouldn't work because it is not unique in certain cases as mentioned. – philosopher Dec 27 '19 at 07:20