objection.js icon indicating copy to clipboard operation
objection.js copied to clipboard

Custom QueryBuilder with "constructor" in TypeScript

Open rhazegh opened this issue 2 years ago • 3 comments

"objection": "^3.0.1"
"knex": "^2.1.0"

I am converting an existing code base from JavaScript to TypeScript. My apologies if this was already answered. I did search the issues but was unable to find an answer so I am going to ask it here.

Existing JavaScript code

My existing JavaScript code for base-model.js works fine and is based on https://github.com/Vincit/objection.js/issues/85#issuecomment-185183032 and looks like this:

import { Model, QueryBuilder, AjvValidator } from "objection";
import addFormats from "ajv-formats";

class DefaultSchemaQueryBuilder extends QueryBuilder {
  constructor(modelClass) {
    super(modelClass);
    if (modelClass.defaultSchema) {
      this.withSchema(modelClass.defaultSchema);
    }
  }
}

export default class BaseModel extends Model {
  static get QueryBuilder() {
    return DefaultSchemaQueryBuilder;
  }

  static createValidator() {
    return new AjvValidator({
      onCreateAjv: (ajv) => {
        addFormats(ajv);
      },
      options: {
        allErrors: true,
        validateSchema: false,
        ownProperties: true,
        v5: true,
      },
    });
  }

  $formatDatabaseJson(json) {
    // Don't forget to call super
    json = super.$formatDatabaseJson(json);

    // do stuff
    // ...

    return json;
  }

  $parseDatabaseJson(json) {
    // Don't forget to call super
    json = super.$parseDatabaseJson(json);

    // do stuff
    // ...

    return json;
  }
}

Converted TypeScript code

My attempt at converting it to TypeScript (base-model.ts) is currently blocked by the errors I am showing in the comments in the code below:

import { Model, Page, QueryBuilder, AjvValidator } from "objection";
import addFormats from "ajv-formats";

class DefaultSchemaQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<
  M,
  R
> {
  ArrayQueryBuilderType!: DefaultSchemaQueryBuilder<M, M[]>;
  SingleQueryBuilderType!: DefaultSchemaQueryBuilder<M, M>;
  MaybeSingleQueryBuilderType!: DefaultSchemaQueryBuilder<M, M | undefined>;
  NumberQueryBuilderType!: DefaultSchemaQueryBuilder<M, number>;
  PageQueryBuilderType!: DefaultSchemaQueryBuilder<M, Page<M>>;

  // I am getting the following warning:
  //
  // (parameter) modelClass: any
  // Parameter 'modelClass' implicitly has an 'any' type, but a better type may be inferred from usage.ts(7044)
  constructor(modelClass) {
    // I am getting the following error:
    //
    // (parameter) modelClass: any
    // Expected 0 arguments, but got 1.ts(2554)
    super(modelClass);
    if (modelClass.defaultSchema) {
      this.withSchema(modelClass.defaultSchema);
    }
  }
}

// I am getting the following error from the linter:
//
// class BaseModel
// Class static side 'typeof BaseModel' incorrectly extends base class static side 'typeof Model'.
// Types of property 'QueryBuilder' are incompatible.
// Type 'typeof DefaultSchemaQueryBuilder' is not assignable to type 'typeof QueryBuilder'.
//   Types of construct signatures are incompatible.
//     Type 'new <M extends Model, R = M[]>(modelClass: any) => DefaultSchemaQueryBuilder<M, R>' is not assignable to type 'new <M extends Model, R = M[]>() => QueryBuilder<M, R>'.ts(2417)
export default class BaseModel extends Model {
  QueryBuilderType!: DefaultSchemaQueryBuilder<this>;
  static QueryBuilder = DefaultSchemaQueryBuilder;

  static createValidator() {
    return new AjvValidator({
      onCreateAjv: (ajv) => {
        addFormats(ajv);
      },
      options: {
        allErrors: true,
        validateSchema: false,
        ownProperties: true,
      },
    });
  }

  $formatDatabaseJson(json: any) {
    json = super.$formatDatabaseJson(json);

    // do stuff
    // ...

    return json;
  }

  $parseDatabaseJson(json: any) {
    json = super.$parseDatabaseJson(json);

    // do stuff
    // ...

    return json;
  }
}

Question:

My question is how would you specify types for the constructor part in base-model.ts:

  constructor(modelClass) {
    super(modelClass);
    if (modelClass.defaultSchema) {
      this.withSchema(modelClass.defaultSchema);
    }
  }

rhazegh avatar Jul 25 '22 19:07 rhazegh

Does anyone have a fix for this, I have the same error.

voidnerd avatar Jul 26 '22 18:07 voidnerd

I found a way around this compile error with @ts-ignore

//@ts-ignore
export class MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<
    M,
    R
> {
    ArrayQueryBuilderType!: MyQueryBuilder<M, M[]>
    SingleQueryBuilderType!: MyQueryBuilder<M, M>
    MaybeSingleQueryBuilderType!: MyQueryBuilder<M, M | undefined>
    NumberQueryBuilderType!: MyQueryBuilder<M, number>
    PageQueryBuilderType!: MyQueryBuilder<M, Page<M>>
    //@ts-ignore
    constructor(modelClass) {
        // @ts-ignore
        super(modelClass)
        this.onBuild(function(builder) {
            if (builder.isFind() && !builder.context().softDelete) {
                builder.whereNull('deleted_at')
            }
        })
    }
}

voidnerd avatar Jul 26 '22 21:07 voidnerd

Do not use contructor. Use forClass static variable.

const buildQueryBuidlerForClass = (scope) => {
  return (modelClass) => {
    const qb = QueryBuilder.forClass.call(scope, modelClass);
    qb.onBuild((builder) => {
       ...
    });
    return qb as any;
  };
};

class YourQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
   ...

   static forClass: ForClassMethod = buildQueryBuidlerForClass(this);
}

keepcosmos avatar Aug 30 '22 02:08 keepcosmos