Solvedobjection.js Typings don't allow custom query builders

I'm trying to create Model, which has extended query builder with .session() method. However when I'm extending the model and setting our custom query builder there return type of $query() and other functions still is QueryBuilder<this> instead of CustomQueryBuilder<this>.

Simplified (not runnable example):

class CustomQueryBuilder<T> extends QueryBuilder<T> {
  session(session: any) {
    this.context().session = session;
    return this;
  }
}

class BaseModel extends Model {
  // Override the objection.js query builders classes for subclasses.
  static QueryBuilder = CustomQueryBuilder;
  static RelatedQueryBuilder = CustomQueryBuilder;

  // ... more stuff ...
}

const daa = await BaseModel
          .query(trx)
          // ERROR: [ts] Property 'session' does not exist on type 'QueryBuilder<BaseModel>'
          .session(req.session)
          .insert({1:1});

Any ideas how to override this? I wouldn't like to do casting everywhere I'm using my extended query builder, nor I would like to add extra methods to BaseModel which would apply correct typing for returned query builder.

56 Answers

✔️Accepted Answer

Here you go:

// Objection types:

interface VoidCallback<T> {
  (t: T): void
}

interface Where<QB> {
  (): QB;
  (cb: VoidCallback<QB>): QB;
  // QueryBuilder<any> is correct typing here and not a workaround
  // because subqueries can be for any table (model).
  <QB2 extends QueryBuilder<any>>(qb: QB2): QB;
}

class QueryBuilder<M extends Model> extends Promise<M[]> {
  where: Where<this>;
}

interface ModelClass<M> {
  new (): M;
}

class Model {
  static query<QB extends QueryBuilder<M>, M extends Model>(this: ModelClass<M>): QB {
    return {} as QB;
  }

  $query<QB extends QueryBuilder<this>>(): QB {
    return {} as QB;
  }
}

// Your types:

class CustomQueryBuilder<M extends Model> extends QueryBuilder<M> {
  customMethod(): this {
    return this;
  }
}

class Person extends Model {
  static query<QB = CustomQueryBuilder<Person>>(): QB {
    return super.query() as any;
  }

  $query<QB = CustomQueryBuilder<Person>>(): QB {
    return super.$query() as any;
  }
}

async function main() {
  const query = Person.query().where().customMethod().where().customMethod();
  takesPersons(await query)

  const persons = await query;
  await persons[0]
    .$query()
    .customMethod()
    .where(qb => {
      qb.customMethod().where().customMethod()
    })
    .where(Person.query().customMethod())
    .customMethod()
}

function takesPersons(persons: Person[]) {

}

Other Answers:

We can finally close this one. Fixed in v2.0 branch. Soon to be released

Good news is that it all seems to work. The bad news is that the types need to be completely rewritten.

So for anyone (most people) that didn't read my comment diarrhea, this is how you would create a custom query builder after the types have been rewritten:

class CustomQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
  // Unfortunately these three types need to be hand-written 
  // for each custom query builder.
  AQB: CustomQueryBuilder<M, M[]>
  SQB: CustomQueryBuilder<M, M>
  NQB: CustomQueryBuilder<M, number>

  customMethod(): this {
    return this;
  }
}

class BaseModel extends Model {
  static QueryBuilder = CustomQueryBuilder;
  // Unfortunately this is needed in addition to the `QueryBuilder` property.
  QB: CustomQueryBuilder<this>
}

class Person extends BaseModel {
  firstName: string;
  lastName: string;
}

The custom query builder type is correctly inherited over any method call, callback, subquery, etc. With the new types you even get autocomplete for properties when you start to type stuff like .where('. I think the new types will be much cleaner too since we don't need to drag around three template arguments. Instead, we only drag around one, the query builder type.

@falkenhawk What's with the eyes? 😄

God Bless you, JTL. I think it would be fun to hang out with you. 🍻I didn’t read any of this but it’s great

I just can't agree with that perspective, for a number of reasons. I have three main points that I'll list to keep it succinct:

  1. those expectations are extremely high for basically any project in existence, and yet this is open source. When something isn't absolutely mission-critical with billions of dollars involved, it very rarely has that level of scrutiny.
  2. typescript doesn't have enough capability to handle some of the typing structures needed that are well suited for ORMs (i don't agree with abandoning patterns in javascript that are a good fit, because the typing system can't handle it, especially when the types can be "good enough")
  3. Personally, I'd much rather have a solid ORM which is good at run-time, and I can be confident about that aspect, rather than focusing on the types as much. Fully typed libraries are great and i'd love an ORM to exist that you describe, but I came from TypeORM; it has more detailed types, but it's also a giant mess with some rather large runtime bugs that are completely unnacceptable for me.

As a result of those three, that's why I'm putting effort into helping with Objections types primarily - I want to get them to a state where people can be confident about their correctness/robustness, as much as is possible with what typescript offers. My goal is to have all of the happy path covered correctly; anything not in the happy path I'm trying to get as much as is possible handled by types as well. Things like what functions can be called, at what time, on the query builder are not something that can be accounted for at this time, for example.

Related Issues:

23
objection.js Support for knex 0.95.0
I'll soon release the first alpha version of objection 3 It supports the newest knex To install it r...
19
objection.js Class constructor Model cannot be invoked without 'new'
You can also make it so that babel is using your node version by doing something like this: ...
15
objection.js Typings don't allow custom query builders
Here you go: I'm trying to create Model which has extended query builder with .session() method ...
12
objection.js Need to explicit destroy knex in order to stop the script
add idleTimeoutMillis: 500 too if you want to use knex in jest Hi I initialize Model this way: The p...
10
objection.js Poll - What would you like to see in the future versions of objection?
Support for for await (...) loops would be nice Especially if it's backed by cursors This could fall...
5
objection.js Timestamps once again
Ah it didn't work because in my BaseModel: ...and in my actual model: ...in case anyone else finds t...