Automattic / mongoose

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

Avoid global plugins in model #9780

Closed chumager closed 2 years ago

chumager commented 3 years ago

Do you want to request a feature or report a bug? Feature What is the current behavior? Hi, I've a module who uses mongoose as mutex (with it's own schema and model), to validate the mutex is taken I just insert the document with a unique index, so if I get an E11000 error I know the mutex is taken.

In other hand I've a plugin who checks if a document with unique index can be saved, so instead of throwing a E11000 error it throws a validation error. My module must be agnostic to the plugins.

Is there a way to tell a schema/model not to apply a global plugin? If the current behavior is a bug, please provide the steps to reproduce.

"use strict";
const mongoose = require("mongoose");

//comment 2 lines above to make it work
const beautifyUnique = require("mongoose-beautiful-unique-validation");
mongoose.plugin(beautifyUnique);
//a simplified mutex
const mutex = new mongoose.Schema({
  name: {
    type: String,
    unique: true,
    required: true,
  },
});
mutex.static({
  async getMutex(name) {
    try {
      const localMutex = await this.create({name});
      return localMutex;
    } catch (err) {
      if (err.code === 11000) {
        const localError = new Error("mutex can't be acquired");
        localError.name = "mutexError";
        throw localError;
      }
      throw err;
    }
  },
});
mutex.method({
  async release() {
    return await this.remove();
  },
});
const Mutex = mongoose.model("Mutex", mutex);
async function doSomething() {
  try {
    const localMutex = await Mutex.getMutex("test");
    //inner logic...
    //wait for a while
    setTimeout(() => localMutex.release(), 100);
    return true;
  } catch (err) {
    if (err.name === "mutexError") {
      return false;
    }
    //this could be any other logic error
    else throw err;
  }
}
async function main() {
  await mongoose.connect("mongodb://localhost/test", {
    useUnifiedTopology: true,
    useNewUrlParser: true,
    useCreateIndex: true,
  });
  await Mutex.deleteMany(); //just for his test
  //doSomething twice
  for (let i = 0; i < 2; i++) {
    doSomething().then((res) => {
      if (res) console.log(i, "DONE");
      else console.warn(i, "NOT DONE");
    }, console.error);
  }
  await new Promise((res) => setTimeout(res, 200));
  doSomething().then((res) => {
    if (res) console.log("LAST DONE");
    else console.warn("LAST NOT DONE");
  }, console.error);
}
main();

In my code the plugin and the model doesn't know about each other and never will, this is only one example, I've several problems like this.

What is the expected behavior?

That I could use a external module who creates a model and this model don't get global plugins applied. maybe in the schema options like {plugins: false} or something like that.

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.

mongoose: 5.11.10 mongo: 4.2 node: 14.15.4

stalniy commented 3 years ago

Global plugins are not applied after compilation. You may use this to achieve desired behavior

stalniy commented 3 years ago
const schema = Schema(...)
const m = mongoose.model(“Mutex”, schema)

mongoose.plugin(...) // this won’t be applied to Mutex
chumager commented 3 years ago

Thanks @stalniy for the comment, but all related with mongoose it's attached to my fw independently, so as this solutions works it'll implies i'd have to change all my inner logic to load other modules, for example some dev uses my fw and loads the mutex module, it'll be loaded after the fw global plugins.

I think this change could be easy because it's an if before load plugins, maybe the mongoose logic ain't allow it, but if it's easy to develop I think it would be nice.

Regards.

vkarpov15 commented 3 years ago

You can also tell the plugin to explicitly avoid a certain schema:

const plugin = require('myplugin');

mongoose.plugin((schema, options) => {
  if (schema === schemaToExclude) {
    return;
  }
  return plugin(schema, options);
});
chumager commented 3 years ago

Yes I know, I can do it, but what about a developer who uses my module?, my issue is to avoid that anyone who uses the module ain't have to acknowledge that he needs to do this, I'm trying to solve a generic problem, if a module must not use any global plugin, is there a way to do it?

Regards.

chumager commented 2 years ago

Hi @vkarpov15 how are you?

Any news about this?

I think this could be something like Schema.set("noGlobalPlugin", true)

I'm developing 3 modules, mutex, semaphore and queue, all for multi servers, using mongoose to keep the data structure, this modules uses error control flow, so any mongoose plugin that will change errors will make those modules to fail.

Regards.

vkarpov15 commented 2 years ago

@chumager no progress yet, but we can add a tags system for v6.1.0. Something like:

Schema.set('tags', ['mutex']);

And

mongoose.plugin(mutexPlugin, { tags: 'mutex' }); // Only apply plugin to schemas with 'mutex' tag

Would that help?

chumager commented 2 years ago

Hi @vkarpov15, I already solved for mi FW, my problem is if someone uses my modules, I added caveats in the readme.

instead of mongoose.plugin(plugin, options) I added a wrapper.

//for each global plugin
mongoose.plugin((schema, options)=>{
  if (!schema.get("noGlobalPlugin")) schema.plugin(plugin, options);
}, options);