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

Fix/method binding on knex proxy #3717

Merged

Conversation

briandamaged
Copy link
Collaborator

The make-knex.js module was taking a few shortcuts when creating the knex proxy. As a result, most (all?) of the methods within the context object were bound to knex instead of context. This would then cause reference errors when one context method attempted to call another context method.

... 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) {
Copy link
Collaborator Author

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,
},
};
Copy link
Collaborator Author

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)

knex[m] = function(...args) {
return this.context[m](...args);
};
}
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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.
Comment on lines 125 to 148
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);
});
}
Copy link
Collaborator Author

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.

@briandamaged briandamaged changed the title WIP: Fix/method binding on knex proxy Fix/method binding on knex proxy Mar 10, 2020
// 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.)
Copy link
Collaborator Author

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 QueryBuilders?

trxClient.transaction(resolve, options, trx).catch(_reject);
});
}
return this._transaction(container, options, trx);
Copy link
Collaborator Author

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!

@briandamaged
Copy link
Collaborator Author

In the near-term, this PR just fixes the this pointer within the context methods.

Longer-term, this also allows us to treat context like a regular Object or Class. Most importantly, this means that we can subclass it rather than patching its methods on the fly. For example:

// Default implementation
class Context {
  constructor() {
    // ...
  }

  transaction(container, config) {
    // ...
  }
}

// Implementation used specifically by Transactions
class TransactionContext extends Context {
  constructor({outerTx}) {
    super();
    this.outerTx = outerTx;
  }

  transaction(container, options) {
    return this._transaction(container, options, this.outerTx);
  }
}

Of course, there are pros and cons w/ this approach. But, the possibility will at least be open to us now.

@kibertoad
Copy link
Collaborator

@briandamaged The reason why I'm concerned about using actual Proxy here is performance. See this: https://thecodebarbarian.com/thoughts-on-es6-proxies-performance

@briandamaged
Copy link
Collaborator Author

@kibertoad : Agreed about the Proxy performance. That's the main reason I decided to omit that part of the refactoring.

@briandamaged
Copy link
Collaborator Author

@maximelkin : do you also want a chance to look at this PR before it gets merged? Or, should we go ahead and merge it?

@maximelkin
Copy link
Collaborator

Go ahead, if this covered by tests

@briandamaged
Copy link
Collaborator Author

@maximelkin : Thx for reviewing it!

@briandamaged
Copy link
Collaborator Author

Oh -- and thx @kibertoad as well!

@briandamaged briandamaged merged commit 37d9c30 into knex:master Mar 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants