-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
refactor(hooks): refactor hooks to use composition (#10880) #12160
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it and it even shaves off soo much code not needed to be maintained anymore!
this.options.hooks = {}; | ||
_.map(hooks || {}, (hooksArray, hookName) => { | ||
constructor(hooks, parent) { | ||
this.hooks = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like would be a good use-case for a Map
😁 But it's not really the scope of this PR so it's not blocking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally maps would be used everywhere instead of objects, prototype pollution is becoming more and more of an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely. Also, it has a performance benefit to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maps are faster than objects?
I'll need some numbers 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eseliger I was interested and gave it a test. Maps have a faster delete, but if just doing a set, then objects are faster.
https://jsperf.com/sequelize-map-vs-obj-2/1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting :)
I also added reading the values in again, seems to still be faster (for those who are interested)
https://jsperf.com/sequelize-map-vs-obj-2/3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, in my yesterday benchmarks Maps were universally faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What operation were you looking at?
04efb3e
to
ab45588
Compare
* | ||
* @param {string} name | ||
* @param {Function} fn A callback function that is called with instance, options | ||
* @name beforeValidate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should preserve / update this documentation. It can be placed alongside respective hooksType
entry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do, while I'm there, do you mind if we remove the params: <NUMBER>
bit?
I just noticed this data is not used at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
param
indicate how many arguments are passed to hook, so you can deal with those arguments based on this data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove param
if documentation is kept with hook definition, thus clearly stating what arguments are being passed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do, however expressing that 'clearly' is rather difficult with the limitation of jsdoc.
The typings will provide the most accurate API documentation here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just state what kind of object is being passed, jsdoc can handle that very well. Old docs from this file can be a starting point. Not everyone is using Typescript so we can't treat them as documentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sushantdhiman I did recover the documentation, probably not like you envisioned it though.
Feel free to give me an example because I really don't use jsdoc and have no idea how to model this accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/**
* A hook that is run after validation
*
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
* @name afterValidate
* @memberof Sequelize.Model
*/
This is what I was thinking, for each definition in hookTypes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving away from mixin to this new approach is fine for me, Although I have to point out this is an unnecessary breaking change. We can perhaps deprecate old API first and introduce this new API alongside.
As mentioned in other comment, having this
referring to current model / sequelize reference is useful and I don't think we should remove that feature
* refactor(hooks): remove method aliases for hooks BREAKING CHANGE: In order to streamline API: - All method style add hook functions have been removed in favor of a composition based approach. - Hook names have been removed, you can add and remove them by function reference instead which was supported before. - Another notable change that `this` inside of hooks no longer refers to the the the hook subject, it should not be used. This affects `Model`, `Sequelize` and `Transaction`. Before: `MyModel.beforeCreate(...)` After: `MyModel.hooks.add('beforeCreate', ...)` Before: `MyModel.addHook('beforeCreate', ...)` After: `MyModel.hooks.add('beforeCreate', ...)` Before: `MyModel.removeHook('beforeCreate', ...)` After: `MyModel.hooks.remove('beforeCreate', ...)` Before: `transaction.afterCommit(...)` After: `transaction.hooks.add('afterCommit', ...)` Before: ```js MyModel.addHook('beforeCreate', 'named', fn); MyModel.removeHook('beforeCreate', 'named'); ``` After: ```js MyModel.hooks.add('beforeCreate', fn); MyModel.hooks.remove('beforeCreate', fn); ``` Before: `MyModel.addHook('beforeCreate', function() { this.someMethod(); });` After: `MyModel.hooks.add('beforeCreate', () => { MyModel.someMethod(); });`
4c952eb
to
00057ac
Compare
|
||
This affects `Model`, `Sequelize` and `Transaction`. | ||
|
||
#### Composition |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you calling these composition? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lack of a better word since the hooks the hooks are now a composition rather than a mix in, probably should call this Examples
instead.
@@ -5,50 +5,96 @@ const { logger } = require('./utils/logger'); | |||
const debug = logger.debugContext('hooks'); | |||
|
|||
const hookTypes = { | |||
beforeValidate: { params: 2 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also consider Model.hooks.beforeValidate(() => {])
in addition to Model.hooks.add('beforeValidate', () => {})
, thus adding these methods directly in Hooks
class. Syntax is bit shorter
Then we can document each method properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That'd kinda defeat the point of this tho, hooks don't know what they merely act as a promise event emitter.
Hooks that might be present in one context might not be in another.
@@ -19,9 +21,9 @@ class Transaction { | |||
* @param {string} [options.deferrable] Sets the constraints to be deferred or immediately checked. PostgreSQL only | |||
*/ | |||
constructor(sequelize, options) { | |||
this.hooks = new Hooks(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wont this means, now you can do transaction.hook.add('beforeCreate')
only to find it won't work, as only afterCommit
hooks is invoked in this module.
We should find a way to throw with .add
method.
As I mentioned in another comment, if we go with <object>.hooks.<hookName>
approach this can be avoided
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a good point, I tried to avoid this scenario with strict typings but not everyone uses that 😞 , however my comment #12160 (comment) still applies, adding methods to hooks for all possible hooks would be misleading and using inheritance would lead to a lot of bloaty code.
Example:
class ModelHooks extends Hooks {
beforeCreate(a, b, c): Promise<void> {
return this.run(a, b, c);
// A lot of code
}
class SequelizeHooks extends ModelHooks {
// ...
}
I wanted to avoid making hooksspecific to their consumers but I guess this would work, I'd make the
run` method private tho to avoid too much API surface and potential misuse.
@eseliger any chance you'd be interested in picking this up, kinda bust atm. |
I thought it's been merged way ago haha. I can see if I find some time, but no promise |
Hey there :) First of all: Thanks a bunch for your contribution to Sequelize! Much appreciated! A couple of months ago, we have switched from master to main branch as our primary development branch and hence this PR is now outdated :( If you still think this change is making sense, please consider recreating the PR against ✌️ |
#11727 # Pull Request check-list
Please make sure to review and check all of these items:
npm run test
ornpm run test-DIALECT
pass with this change (including linting)?Description of change
Salvaged from previously suggested changes:
BREAKING CHANGE:
In order to streamline API:
this
inside of hooks no longer refers to the the the hook subject, it should not be used.This affects
Model
,Sequelize
andTransaction
.Before:
MyModel.beforeCreate(...)
After:
MyModel.hooks.add('beforeCreate', ...)
Before:
MyModel.addHook('beforeCreate', ...)
After:
MyModel.hooks.add('beforeCreate', ...)
Before:
MyModel.removeHook('beforeCreate', ...)
After:
MyModel.hooks.remove('beforeCreate', ...)
Before:
transaction.afterCommit(...)
After:
transaction.hooks.add('afterCommit', ...)
Before:
After:
Before:
MyModel.addHook('beforeCreate', function() { this.someMethod(); });
After:
MyModel.hooks.add('beforeCreate', () => { MyModel.someMethod(); });