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

Integration for ES6 classes #4668

Merged
merged 2 commits into from
Nov 23, 2016
Merged

Integration for ES6 classes #4668

merged 2 commits into from
Nov 23, 2016

Conversation

kherock
Copy link
Contributor

@kherock kherock commented Oct 30, 2016

The reason for this PR is that I noticed that mongoose under the hood is essentially acting as a mixin to a class, and I saw room for improvement with the syntax for defining models. Generally static and instance methods have to be inconveniently defined through helpers on a Schema before they are copied onto the model, and Model.compile is also being inefficient by manually setting the prototype. So essentially, mongoose is doing work that ES6 already natively supports.

With these changes mongoose.model can support taking an ES6 class where name is normally provided:

const mongoose = require('mongoose');
const { Model, Schema } = mongoose;

const schema = new Schema({
  _id: Schema.ObjectId,
  field1: Number,
  field2: String
});
class SomeThing extends Model {
  static explode() {
    return SomeThing.explode();
  }

  get numString() {
    return this.field1 + this.field2;
  }

  increment() {
    return SomeThing.update({ _id: this._id }, {
      $inc: { 'field1': 1 }
    }, { new: true }).exec();
  }
}

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

I've also included a loadClass instance method for schemas that accomplishes some of what mongoose-class-wrapper can do. It allows similar functionality as the above, but it's useful for sub-schema definitions.

This should be fully backward compatible and shouldn't introduce any breaking changes.

@kherock
Copy link
Contributor Author

kherock commented Nov 4, 2016

Just looking ahead even further, eventually the Schema constructor could be an @model class decorator if we opt to support Babel. This would mean we don't have to manually call mongoose.model at the end and we could have something like

@model({
  _id: Schema.ObjectId,
  field1: Number,
  field2: String
}, { collection: 'some_things' })
export class SomeThing extends Model { ... }

which IMO looks very nice.

}

// Add static methods
if (!virtualsOnly) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm what's the point of this virtualsOnly option? Looks like it's always true anyway...

Copy link
Contributor Author

@kherock kherock Nov 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it's probably unnecessary. Its only use is mainly internal by Model.compile. That's also why I didn't add JSDoc for the parameter.

It seems like Schemas need the virtuals defined on them directly for them to function properly, e.g. in toObject and toJSON. It just felt wasteful copying all of the statics and instance methods from the class onto the schema when applyMethods(model, schema) and applyStatics(model, schema) are being called immediately afterward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, loadClass is intended to be public, used generally when someone might want to use a class to define virtuals, etc. on a subschema, like

const subSchema = new Schema({ ... });
subSchema.loadClass(class { ... });

const schema = new Schema({ field1: subSchema });
class MyModel extends Model { ... }

In this case virtualsOnly is undefined.

if (typeof name === 'function') {
model = name;
name = model.name;
if (!(model.prototype instanceof Model)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong.. I don't think this would work in the case when using a Base class on an individual class.

export class Foo extends MyBase {...}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, instanceof looks through the whole prototype chain. So as long as MyBase extends Model, it's fine. I'm actually currently using this use of extends for some of the models in my app.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clarifying that. I didn't know that about instanceof; obviously :)

@@ -393,7 +401,7 @@ Mongoose.prototype.model = function(name, schema, collection, skipInit) {
}

var connection = options.connection || this.connection;
model = this.Model.compile(name, schema, collection, connection, this);
model = this.Model.compile(model || name, schema, collection, connection, this);
Copy link
Contributor

@mleanos mleanos Nov 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change needed? name is being set above when model is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where the problem you see is, can you elaborate? model is unset if the name passed in is a string, which means that Model.compile receives a string name rather than an ES6 class, and everything proceeds normally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I just saw the diff and thought it wasn't necessary based on my misunderstanding of what the compile method was doing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I first looked at this, I thought this bit was still inside the if (typeof name === 'function').. scope.

@mleanos
Copy link
Contributor

mleanos commented Nov 21, 2016

I'm very interested in having this feature. I would like to decouple my application's classes from my data access layer. If I can provide an es6 class in lieu of the Mongoose Schema definition, then I can effectively use the same classes with a different database technology.

Without this PR's implementation, is there a recommend way of achieving this?

@kherock
Copy link
Contributor Author

kherock commented Nov 21, 2016

@mleanos Yeah, that is the direction I'd like to see mongoose go.

This is mostly attainable using the mongoose-class-wrapper plugin, but obviously it's less convenient and uglier, and IDEs that support ES6 don't aren't aware of the methods provided by Model.

@vkarpov15 vkarpov15 added this to the 4.7.0 milestone Nov 23, 2016
@vkarpov15 vkarpov15 merged commit abd58d8 into Automattic:master Nov 23, 2016
@malixsys
Copy link

@mleanos this is now merged and released in 4.7

@malixsys
Copy link

@rockmacaca 👏 👏 👏 Thank you for this!

@TrejGun
Copy link
Contributor

TrejGun commented Nov 25, 2016

🎉

@vkarpov15
Copy link
Collaborator

🎉

@klimashkin
Copy link

Is there docs for this feature?
How to deal with path, pre, etc?

@kherock
Copy link
Contributor Author

kherock commented Jan 6, 2017

@klimashkin I suppose there probably should be an example like the one I initially gave in the API docs. This shouldn't affect anything with path or pre, since those are for schemas.

However, depending on how you're set up, you might be able to natively add pre and post hooks like this:

class MyClass extends Model {
  async save() {
    doSomethingPreSave();
    const newDoc = await super.save();
    doSomethingPostSave();
    return newDoc;
  }
}

@aksyonov
Copy link

Awesome 🎉

@alexis-y
Copy link

This is great, I've been looking how to do this for ages!

However I can't get it to work with discriminators/schema extension.

Here's what I have (roughly):

mongoose.model(
  class LoginBase extends Model {}, 
  new Schema({ enabled: Boolean }, { collection: 'logins', discriminatorKey: '_type' })
);

LoginBase.discriminator(
  class LocalLogin extends LoginBase {},
  new Schema({ email: String, password: String })
);

This throws Virtual path "__v" conflicts with a real path in the schema. If I disable versioning, then it's "_id" that fails. At that point it's clear either I've got something backwards, or this use case isn't supported (in which case I'd be glad to help out)

@vkarpov15
Copy link
Collaborator

@alexis-y can you please open up a separate issue? Hard to track issues when new issues are nested underneath old ones :)

vkarpov15 added a commit that referenced this pull request Apr 16, 2017
@malcommac
Copy link

Is there a doc to see how it should work?

@vkarpov15
Copy link
Collaborator

http://mongoosejs.com/docs/advanced_schemas.html

@NickBolles
Copy link

I assume this will work with typescript classes, but I don't see any mention of it. Will this work? Also as a side question, can you define the constructor in the class to extend the base Model constructor? Again it would seem so.

@everythingspirals
Copy link

everythingspirals commented Mar 26, 2018

When I use the syntax from the original post, i.e:

class UserSession extends mongoose.Model {
    static get MAX_LOGIN_ATTEMPTS() {
        return 5;
    }

    static get TOKEN_EXPIRING_PERIOD() {
        return 172800;
    }
}

mongoose.model(UserSession, schema, 'user.session');

I get the following error:

MissingSchemaError: Schema hasn't been registered for model "user.session".

However, if I revert to:

mongoose.model('user.session', schema, 'user.session');

I no longer get this error.

var model;
if (typeof name === 'function' && name.prototype instanceof Model) {
model = name;
name = model.name;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the '.name' defined?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because type of name=== 'function' and model = name model is a function. This function.prototype.name is what it's calling. So it would be "myFunction" in the below function function my function() {}

With a class this is the name of the class.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we want our model to have a name different than the class name, i.e if our model is name is 'my.function'?

We have to override it? i.e

class myFunction {
     static get name(){
          return 'my.function';
     }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is a read-only property, so you can't change it
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name

I suggest you look at the link that vkarpov15 commented to use .LoadClass into the schema you want

@benjamin-sweney
Copy link

benjamin-sweney commented May 1, 2018

@everythingspirals problem lies in /lib/index.js:390. Specifically:

if (options.cache === false) {
    return model;
}

Removing this allow it to work as expected. Not 100% positive on submitting a pull request as I'm sure there's a deeper issue but I myself do not have time to look into it. Perhaps a contributor like @vkarpov15 could look into this?

@vkarpov15
Copy link
Collaborator

@benjamin-halter can you provide a script that demonstrates the issue you're seeing? That'll make it much easier for me to look into this.

@abettke
Copy link

abettke commented Mar 22, 2019

http://mongoosejs.com/docs/advanced_schemas.html

In regards to this in case anyone else runs into this. If you are using a model class to define document methods, use the schema.loadClass() method to correctly integrate it into the schema. Not doing so creates some weird behavior with schemas hooks being registered multiple times.

class Test extends mongoose.Model {
    ....
}
const schema = new mongoose.Schema('testschema', {
	...
})
  /** Middleware */
  .pre('save', async function () {
     ... some stuff
  })
  .loadClass(Test); // GOOD

-----------

mongoose.model(Test, schema); // BAD, pre-save middleware calls twice

@Automattic Automattic locked as resolved and limited conversation to collaborators Mar 24, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.