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
Feat: Allow to extend knex query builder #3334
Conversation
This feature is wanted and it has been discussed before. I want to research earlier discussions and review the code, since for this I want knex to have carefully designed api and functionality. This fundamental features should have design discussions before doing PRs or to be prepared to redo the whole implementation in a different manner. |
Since my implementation is small one i would happy to redo it. |
TBH, this seems a simple and non-intrusive solution, so I would vote for including this implementation. One additional suggestion: it might make sense to throw a warning when you are overriding existing method using .extend() |
There doesn't seem to be many valuable proposals in prior issues, TBH. |
I can check, TS by design allows to extend interfaces, but there some case it won't work. |
Agree, it can be a base for more pluggable solution (if needed). What do you think regarding exposing |
}); | ||
|
||
it('should extend default queryBuilder', (done) => { | ||
Knex.QueryBuilder.extend('customSelect', function(value) { |
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 leaks extended querybuilder outside of thus test, I think. Would be good to delete custom metode in after() block.
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.
So you suggest to add remove method as well? cause with the current api, it always adds the method for the entire app.
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.
wouldn't simple delete Knex.Querybuilder.prototype.customMethod
work? I don't think we should expose deletion method via API.
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 will work, I will update my PR soon
@kibertoad I'm not sure how to test the type augmentation of the QueryBuilder extension, can you give me some pointers? |
done(); | ||
}); | ||
}); | ||
|
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 should be also tests that custom method is available for example in transaction and maybe in some cases one likes to add it to join builder or some nested subquery builder.
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.
Not sure I'm following, the extend
is done on the Builder.prototype
only once in the app (before any querying).
There is no difference if you are using the custom method with sub-query or any other chain.
I'm missing something?
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.
Transaction is a different entity, not an original knex builder, but user would be justified to expect custom methods on it as well. It is highly likely we'd need to copy them over, but test would show that. Same with join builder, it's a different thing.
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.
So, should I add extend
to them as well? should it be separate method for each?
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.
No, it would be better if they were inherited automatically. User needs not to concern himself with difference between all three.
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.
You can use transaction pretty much same way you can use knex instance. See example from documentation:
// Using trx as a transaction object:
const trx = await knex.transaction();
const books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
trx('catalogues')
.insert({name: 'Old Books'}, 'id')
.then(function(ids) {
books.forEach((book) => book.catalogue_id = ids[0]);
return trx('books').insert(books);
})
.then(trx.commit)
.catch(trx.rollback);
})
Considering that currently transaction supports same query-building methods that knex.QueryBuilder does, as a user I would be very surprised if I'm supposed to set those up separately.
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.
Although looking at the code, I suspect this is going to just work (test to prove that would be most welcome, though). The way transactor is created is this:
const transactor = makeKnex(trxClient);
So what gets called is this:
function makeKnex(client) {
// The object we're potentially using to kick off an initial chain.
function knex(tableName, options) {
return createQueryBuilder(knex.context, tableName, options);
}
redefineProperties(knex, client);
return knex;
}
Unless I'm missing something, this is going to call same QueryBuilder constructor in the end.
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.
Ha, I'm not using transaction in that way ;)
knex.transaction((trx) => {
knex(t1).transacting(trx)...
})
If it has the same query methods, it make sense to extend it as well.
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.
trx
var in this context is the same transactor you would get from await knex.transaction()
, and see above, I suspect it already works.
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.
Let me add a test for it to assure it, if it not works we will need to think how extend it as well
@felixmosh Regarding testing: not sure, never tested for this case myself, but from my understanding it is possible to assert literal types for specific lines. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v8/node-tests.ts |
Sounds like only the issue #1158 had some really meaningful discussion around the area. Right now I think that the only drawback with this extension method is to be able to nicely override old methods with extended functionality. Being able to call original implementation of rewritten method sounds like something that I would like to have (I've used that quite a lot with objection). I think that for now we could add this way that just allows to add stuff to prototype and pass it around to other builders as experimental extension support (to be able to deprecate it fast if it is not sufficient). From that experience we can decide later on if we actually want to make extending to work with real inheritance and setting then query builder instances to knex in a bit same way that it was suggested here #1158 (comment) and how it was implemented for objection). |
For transaction all the same methods should be found that are found from main query builder... so all use cases I suppose :) For join builder and subquery builder probably doesn't need to have all the extensions... I suppose extender method could have |
Then another thing which needs testing is to check that instance created with |
@elhigu Given that joinBuilder has methods along the line of
, is that really related to extending QueryBuilder per se? Probably there could be separate method exposed for extending just the JoinBuilder, but I would say that would be a separate PR altogether. |
Separate PR sounds fine if that even is ever needed. I agree that it doesn't sound really important. |
Sorry for the confusion, what are the action items? :) |
|
Thanx, will work on it at the evening. |
@felixmosh Sounds good, as long as you think that could be introduced later in a non-breaking way. |
Lets mark this support experimental, so it will be ok to break when we have more user experience about how it should work. |
Agree, how do i mark it as experimental? |
Just by mentioning it in documentation |
Unfortunately, this needs to be updated to accomodate for /src -> /lib movement of knex files. Should be fairly easy fix, though. |
No problem, i will fix that at the weekend. |
types/test.ts
Outdated
@@ -4,6 +4,12 @@ import * as Knex from 'knex'; | |||
// import Knex from 'knex' | |||
// when "esModuleInterop": true | |||
|
|||
declare module 'knex' { | |||
interface QueryBuilder { | |||
onDuplicateUpdate(...columnNames: string[]): QueryBuilder; |
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.
Are any assertions possible on existence of this method?
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've improved it & added a usage.
:]
A documentation PR knex/documentation#218 |
I am personally not a huge fan of multiple libraries extending shared context, and in general, one import potentially changing the outcome of another import (across files). I think this will also cause jump to definition to not work (even in a pure typescript codebase - it will just jump to the declaration which would have to be separately defined). But given that people see value in this, and also because the current knex codebase is already not very statically explorable (because of not using ES6 classes in some places, extensive aliasing etc. features like jump-to-definition, ide outline etc. often don't work), hence I don't have a very strong opinion here. |
Released in 0.19.1 |
This PR allows to extend query builder with custom functions such as
paginate
oronDuplicateUpdate
that will allow add custom behaviour or specific dialect methods over the basic query builder functions.Currently there is no way to properly extend the QueryBuilder without monkey-patching the
QueryBuilder.prototype
.closes #1158