2

I am working on a project that uses Mongo/Mongoose/NodeJS.

I have a requirement for a relatively complex constraint on the data in the db and I am not sure if it is even possible to implement.

My schema is as follows:

const schema = new Schema(
  {
    id: ObjectId,
    date: {
      type: Date,
      required: true,
      unique: true,
    },
    eventType: {
      type: String,
      enum: ['GROUP_STAGE', 'ROUND_OF_16', 'QUARTER_FINAL', 'SEMI_FINAL', 'FINAL'],
      required: true,
    },
  },
);

The required logic is:

An eventType can only exist once per calendar year, unless the eventType is 'GROUP_STAGE' which can exist multiple times in a calendar year.

I'm pretty inexperienced with Mongo/Mongoose, however I'm getting the feeling this restriction may not be possible.

I can put the logic in the NodeJS layer before insert/update if that is the only way to do it, however that feels a bit hacky and brittle.

If the answer to this requirement is "it can't be done" then I would still be grateful for the confirmation.

EDIT - Solution by following the advice in the accepted answer

const schema = new Schema(
  {
    id: ObjectId,
    date: {
      type: Date,
      required: true,
      unique: true,
    },
    eventType: {
      type: String,
      enum: ['GROUP_STAGE', 'ROUND_OF_16', 'QUARTER_FINAL', 'SEMI_FINAL', 'FINAL'],
      required: true,
    },
  },
);

const getFindDuplicateEventTypeInASingleYearQuery = (values) => {
  const { id, eventType, year } = values;
  const query = {
    eventType,
    date: {
      $gte: new Date(`${year}-01-01T00:00:00.000Z`),
      $lt: new Date(`${year}-12-31T23:59:59.999Z`),
    },
  };
  // If an id is supplied, exclude it from the query
  // This is to prevent false flags when updating an object
  if (id) {
    query._id = { $ne: new mongoose.Types.ObjectId(id) };
  }
  return query;
};

const checkForInvalidDuplicateEventTypeInYear = async (queryValues, model) => {
  if (queryValues.eventType !== 'GROUP_STAGE') {
    const query = getFindDuplicateEventTypesInASingleYearQuery(queryValues);
    const count = await model.countDocuments(query);
    if (count > 0) {
      return new Error(`There is already a fixture with the eventType ${queryValues.eventType} in ${queryValues.year}`);
    }
  }
  return null;
};

async function handlePreSave(next) {
  next(
    await checkForInvalidDuplicateEventTypeInYear(
      {
        eventType: this.eventType,
        year: this.date.getFullYear(),
      },
      this.constructor,
    ),
  );
}

async function handlePreFindOneAndUpdate(next) {
  const { eventType, date } = this.getUpdate();
  next(
    await checkForInvalidDuplicateEventTypeInYear(
      {
        id: this.getQuery()._id,
        eventType,
        year: date.split('-')[0],
      },
      this.model,
    ),
  );
}

schema.pre('save', handlePreSave);
schema.pre('findOneAndUpdate', handlePreFindOneAndUpdate);

module.exports = mongoose.model('Fixture', schema);



SamF
  • 255
  • 1
  • 3
  • 16
  • 1
    you can have a pre-save hook to apply this constrain. Please check the [reference](https://medium.com/@justinmanalad/pre-save-hooks-in-mongoose-js-cf1c0959dba2) – Nayan May 01 '20 at 10:34
  • Thank you for responding! I've taken a look at pre and it seems useful - would the 'correct' thing to do be to run a query against the existing documents from within the 'pre' function and use the result of the query to compare with the document to be inserted? Or is there a better way to access existing documents from within a 'pre' function? – SamF May 01 '20 at 14:11
  • Added few more resources and idea in the answers. Hopefully that might help. – Nayan May 01 '20 at 14:31

1 Answers1

1

A pre-save hook to apply this constrain will be helpful. Please check the reference

You can pass the data of the constrain result and do action according to that like the document should be saved or not.

To pass the data in pre and post save you can check this

And more on how exactly middleware works. Reference

Nayan
  • 638
  • 6
  • 15
  • Thank you very much for the information. The one thing I am struggling with is how to compare the properties of the document that I am inserting to the properties of documents already in the database? As in, I am wanting to insert { date: '01-01-2020', 'eventType': 'FINAL' } and need to check that there are not any other entries in the DB for the year 2020 with an eventType of final – SamF May 01 '20 at 16:21
  • So in the pre save hook you have access to req object from there you can get the data you want to enter. Then you can use find query to see if any doc is there which satisfy the condition. You need help with that query? – Nayan May 01 '20 at 16:47