-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Fix/method binding on knex proxy #3717
Fix/method binding on knex proxy #3717
Conversation
... as opposed to injecting the methods directly onto the knex function. (Which was then causing `this` to point to the wrong object when evaluating the context methods)
... which was possible since all of the properties reference `this` instead of `knex` now
@@ -264,7 +264,7 @@ function makeTransactor(trx, connection, trxClient) { | |||
transactor.isTransaction = true; | |||
transactor.userParams = trx.userParams || {}; | |||
|
|||
transactor.transaction = function(container, options) { | |||
transactor.context.transaction = function(container, options) { |
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.
It looks like this line was only working because of the way the proxy shortcuts had been setup. In other words: one bug was canceling another bug.
So, now that the proxies are being setup correctly, we need to make sure we override the method in the correct place. (on the context
object rather than the knex
function)
}, | ||
configurable: true, | ||
}, | ||
}; |
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 replaced all of the references to knex
with references to this
instead. This means that the property definitions can now be identical for every knex
instance. Therefore, I moved the property definitions into the top-level module so that they could be reused. (Rather than creating a new instance of the definitions for each knex
instance)
lib/util/make-knex.js
Outdated
knex[m] = function(...args) { | ||
return this.context[m](...args); | ||
}; | ||
} |
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 was really the key change. Previously, these methods were not actually being proxied. Instead, they would be injected directly into the knex
function. For example:
knex.transaction = context.transaction;
knex.raw = context.raw
The problem: this meant that the methods would be invoked through knex
now instead of context
. Thus, this
would point to knex
instead of context
. As a result, it was impossible to define methods in context
such as:
const context = {
transaction(container, _options) {
// Do some implementation-specific stuff
const outerTx = createOuterTxForTopLevelTransactions();
// This line would fail because `this` is pointing to `knex` instead of `context`!
return this._transaction(container, options, outerTx);
},
_transaction(container, options, outerTx) {
// Do stuff that is common across all implementations...
}
}
@@ -1089,7 +1089,7 @@ module.exports = function(knex) { | |||
}); | |||
it('should capture stack trace on raw query', () => { | |||
return knex.raw('select * from some_nonexisten_table').catch((err) => { | |||
expect(err.stack.split('\n')[2]).to.match(/at Function\.raw \(/); // the index 2 might need adjustment if the code is refactored | |||
expect(err.stack.split('\n')[2]).to.match(/at Object\.raw \(/); // the index 2 might need adjustment if the code is refactored |
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.
Previously, the stacktrace said Function
here because the underlying method was not actually being proxied. Instead, it was being invoked through knex
, which was a Function.
... instead, it uses the normal context property
... This way, we can be sure that the lower-level details are consistent across implementations. Individual implementations just need to handle the quirks around setting up the `config` and `outerTx`
Restructured the code so that CONTEXT_METHODS populates the KNEX_PROPERTY_DEFINITIONS with the proxy methods. In doing so, it also exposed the fact that the withUserParams(..) method was being overridden on the Transactor instead of its context. So, that bug was fixed as well.
transaction(container, _config) { | ||
const config = Object.assign({}, _config); | ||
config.userParams = this.userParams || {} | ||
if(isUndefined(config.doNotRejectOnRollback)) { | ||
config.userParams = this.userParams || {}; | ||
if (isUndefined(config.doNotRejectOnRollback)) { | ||
// Backwards-compatibility: default value changes depending upon | ||
// whether or not a `container` was provided. | ||
config.doNotRejectOnRollback = !container; | ||
} | ||
|
||
return this._transaction(container, config); | ||
}, | ||
|
||
if(container) { | ||
const trx = this.client.transaction(container, config); | ||
// Internal method that actually establishes the Transaction. It makes no assumptions | ||
// about the `config` or `outerTx`, and expects the caller to handle these details. | ||
_transaction(container, config, outerTx = null) { | ||
if (container) { | ||
const trx = this.client.transaction(container, config, outerTx); | ||
return trx; | ||
} else { | ||
return new Promise((resolve, reject) => { | ||
const trx = this.client.transaction(resolve, config); | ||
const trx = this.client.transaction(resolve, config, outerTx); | ||
trx.catch(reject); | ||
}); | ||
} |
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 refactored the transaction(..)
method just to demonstrate that the this
pointer was referencing the correct object now. So, this allows this.transaction(..)
to call this._transaction(..)
without encountering any reference errors.
// construction. However, `KNEX_PROPERTY_DEFINITIONS` is | ||
// constructed before any `knex` instances are created. | ||
// As a result, the method extensions were missing from all | ||
// `knex` instances.) |
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 decided to omit this part from the PR until we have a chance to discuss it.
On one hand, we could probably work around this by using an actual Proxy(..)
. On the other hand, it seems strange that we're allowing knex
data / structures to be directly manipulated in this way. Perhaps we can come up w/ some way to preserve the original QueryBuilder
, yet also allow end-users to create/use custom QueryBuilder
s?
trxClient.transaction(resolve, options, trx).catch(_reject); | ||
}); | ||
} | ||
return this._transaction(container, options, trx); |
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
correctly refers to the transaction.context
now, which means that this method call works now. Hooray!
In the near-term, this PR just fixes the Longer-term, this also allows us to treat
Of course, there are pros and cons w/ this approach. But, the possibility will at least be open to us now. |
@briandamaged The reason why I'm concerned about using actual Proxy here is performance. See this: https://thecodebarbarian.com/thoughts-on-es6-proxies-performance |
@kibertoad : Agreed about the |
@maximelkin : do you also want a chance to look at this PR before it gets merged? Or, should we go ahead and merge it? |
Go ahead, if this covered by tests |
@maximelkin : Thx for reviewing it! |
Oh -- and thx @kibertoad as well! |
The
make-knex.js
module was taking a few shortcuts when creating theknex
proxy. As a result, most (all?) of the methods within thecontext
object were bound toknex
instead ofcontext
. This would then cause reference errors when onecontext
method attempted to call anothercontext
method.