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

Helper for applying multiple plugins #473

Closed
koskimas opened this issue Aug 6, 2017 · 21 comments
Closed

Helper for applying multiple plugins #473

koskimas opened this issue Aug 6, 2017 · 21 comments

Comments

@koskimas
Copy link
Collaborator

koskimas commented Aug 6, 2017

This can become a mess:

class BaseModel extends Plugin1(Plugin2(Plugin3({opt: 'foo'})(Plugin4(Model)))) {

}

Should we add a helper like this:

class BaseModel extends Model.plugin(Plugin1, Plugin2, Plugin3({opt: 'foo'}), Plugin4) {

}

Or would mixin be a better name for the method?

Arrays would also be supported:

class BaseModel extends Model.plugin([
  Plugin1, 
  Plugin2, 
  Plugin3({opt: 'foo'}),
  Plugin4
]) {

}
@devinivy
Copy link
Collaborator

devinivy commented Aug 7, 2017

Worth checking the API/implementation of this lib for inspiration https://github.com/justinfagnani/mixwith.js

If you go the direction you're hinting at, I'd request to at least let Model.plugin(P1, ..., Pn) extend from Model even if Model is already an extension of Objection.Model.

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 7, 2017

@devinivy That library was my inspiration. I originally thought of adding a helper like:

class Person mixin(Model, Plugin1, Plugin2) {

}

but then the user would need to require the mixin helper. Since we are always mixin in stuff to Model or QueryBuilder or some othe known baseclass, we can make the mixin helper a static method.

The implementation would be as simple as this:

class Model {
  static plugin() {
    return _.flatten(_.toArray(arguments)).reduce((Model, Plugin) => {
      return Plugin(Model);
    }, this);
  }
}

Of course it will always extend this instead of objection.Model. The following two are equivalent:

class Person extends Model.plugin(Plugin1, Plugin2) {

}
class Person extends Model.plugin(Plugin1).plugin(Plugin2) {

}

@fl0w
Copy link
Contributor

fl0w commented Aug 7, 2017

Wouldn't it be easier to have a callable api that just reassigns objection.Model with most recent "extends"?

objection.plugin(Plugin1)
objection.plugin(Plugin2)
objection.plugin(Plugin3)

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 7, 2017

@fl0w Nope, then we would lose part of the flexibility. After that you would have no way to access the original unmodified Model. I think this comment by @devinivy explains it well.

@fl0w
Copy link
Contributor

fl0w commented Aug 7, 2017

@koskimas understood. Storing the plugin/model chain layout wouldn't help either?

objection.inspectPluginChain = {
  original: Model,
  'plugin-name-1': Plugin1, 
  'plugin-name-2': Plugin2
}

Extending with intent is always an option but, correct me if I'm wrong, generally plugins would be used application wide? (considering soft-delete/case-conversion/auto-update-timestamps perhaps).

edit I'm not criticising the provided solution, just exploring by poking. Ignore if I'm annoying.

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 7, 2017

generally plugins would be used application wide?

Probably, but nothing prevents you from doing

objection.Model = objection.Model.plugin(Plugin1, Plugin2);

@fl0w
Copy link
Contributor

fl0w commented Aug 7, 2017

Ah, got it. Thanks. That's clean!

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 7, 2017

Would you prefer mixin instead of plugin as the name? I think we could use this kind of helper because many people seem disgusted by

class BaseModel extends Plugin4(Plugin3(Plugin2({opt: 'foo'})(Plugin1(Model)))) {

}

This seems cleaner to me:

class BaseModel extends Model.mixin([
  Plugin1, 
  Plugin2({opt: 'foo'}), 
  Plugin3,
  Plugin4
]) {

}

@fl0w
Copy link
Contributor

fl0w commented Aug 7, 2017

That's sort of reminiscent of a stack of decorators (visually).
I think it's generally easier to list than scope depth, so I personally prefer the latter.

edit, sorry, I prefer "mixin" - though I don't have any strong feelings. Can the term "plugin" be used in different contexts? I think "mixin" is more specific (thus easier to document?).

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 7, 2017

Yeah, mixin is what we are doing. I think that's a better name. I'll still document plugin usage using simple function calls (like it is explained today) so that people see what is going on. I'll only add a side note about the mixin helper for the people that throw up in their mouth.

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Aug 8, 2017

Maybe a simple compose helper like this one can be added for multiple plugins (which are called higher-order functions). It is widely used and tested by react users:

import { compose, Model } from 'objection'

class Animal extends Model {
  static tableName = 'animals'
}

// Single higher-order function
export default timestamps({ type: 'timestamptz' })(Animal)

// Multiple higher-order functions
export default compose(
  timestamps({ type: 'timestamptz' }),
  softDeletes
)(Animal)

Another way is to use decorators(babel stage 2 proposal), though I prefer compose helper:

@timestamps({ type: 'timestamp' })
@softDeletes
class Animal extends Model {
  static tableName = 'animals'
}

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 8, 2017

@vladshcherbin The problem with applying the mixings after extending the parent class is that the class name will be the name of the last applied plugin class. All stack traces containing model methods will have confusing class names. Otherwise you can already do that.

I'm getting the feeling that no-one is especially thrilled about the plugin or mixin helper as a static member of Model. @devinivy Would you prefer a separate helper function in objection core:

import { mixin, Model } from 'objection'

class Animal extends mixin(Model, [Plugin1, Plugin2]) {
  static tableName = 'animals'
}

Or would you prefer no helpers at all?

@vladshcherbin If we go down that road, I could also add a compose function like you suggested.

@devinivy
Copy link
Collaborator

devinivy commented Aug 8, 2017

While it's not a big deal to me, I think it would be useful to have a very basic one in objection to reinforce the standard way to write an objection plugin. I think it would be positive for the objection ecosystem. Whether it's Model.mixin() or mixin(Model), I think it's fine :)

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Aug 8, 2017

@koskimas maybe class name won't be wrong if higher-order function is defined like this one?

function timestamps(options) {
  return WrappedModel => (
    class extends WrappedModel {
      ...
    }
  )
}

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 8, 2017

Try this:

class X {}
const f = (M) => class extends M {}
const Y = f(X);
console.log(Y.name);

and you get '' as an output. The last extending class has no name.

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Aug 8, 2017

@koskimas actually it's giving me X as a result. Here is the jsbin example.

Using babel with env preset set to node 5 or higher, it also gives the same result of X - babel repl. With preset set to node 4 it gives '_class' - babel repl.

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 8, 2017

Node 7 gives an empty string and so does node 4. I doubt that 5,6 or 8 act any different. Babel is not 100% compatible in this case. We can't write features for Babel. They must be written against ecmascript standard and actual native node.

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Aug 8, 2017

@koskimas for me, babel is working exactly the same as node. Here is the same code with pure node 8:

screen shot 2017-08-08 at 11 18 04 pm

@koskimas
Copy link
Collaborator Author

koskimas commented Aug 8, 2017

You're right. Node 8 does work like that. Older versions give the empty string. That's good news, but I suspect that many plugin developers give names to their plugin classes. Maybe we should add this to best practices and encourage people to leave the classes nameless.

@vladshcherbin
Copy link
Contributor

@koskimas yeah, this will be great. With latest node versions and babel you get nice things like async/await, decorators, etc.

Adding plugins also becomes a joy with higher-order functions and a combining helper like compose. This is exactly the way I would add them in my projects.

@maprangsoft
Copy link

@vladshcherbin The problem with applying the mixings after extending the parent class is that the class name will be the name of the last applied plugin class. All stack traces containing model methods will have confusing class names. Otherwise you can already do that.

I'm getting the feeling that no-one is especially thrilled about the plugin or mixin helper as a static member of Model. @devinivy Would you prefer a separate helper function in objection core:

import { mixin, Model } from 'objection'

class Animal extends mixin(Model, [Plugin1, Plugin2]) {
  static tableName = 'animals'
}

Or would you prefer no helpers at all?

@vladshcherbin If we go down that road, I could also add a compose function like you suggested.

thank you very much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants