olavim / objection-cursor

Cursor based pagination plugin for Objection.js
MIT License
30 stars 8 forks source link

Objection v2.x is out, would it be possible to add TypeScript definitions for this plugin? #12

Closed cerinoligutom closed 4 years ago

cerinoligutom commented 4 years ago

Related to #1

olavim commented 4 years ago

I've been trying this out and it's looking promising. I want to make sure I can get typings working with other mixins, and determine backward compatibility before making a release.

By following the discussion here, just rewriting everything in TypeScript and getting (seemingly) proper types for custom query builder was relatively easy. I, however, needed to generate declaration files while transpiling the source to JS (so that it can be used in JS projects), which was giving me trouble to no end.

Here's a minimal example of a plugin with a custom query builder:

type AnyConstructor<A = object> = new (...input: any[]) => A;

export function Mixin(options: {sessionKey: string}) {
    return function <T extends AnyConstructor<Model>>(Base: T) {
        class MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
            public ArrayQueryBuilderType!: MyQueryBuilder<M, M[]>;
            public SingleQueryBuilderType!: MyQueryBuilder<M, M>;
            public NumberQueryBuilderType!: MyQueryBuilder<M, number>;
            public PageQueryBuilderType!: MyQueryBuilder<M, Page<M>>;

            public session(session: any) {
                return this.mergeContext({
                    [options.sessionKey]: session
                });
            }
        }

        return class extends Base {
            public static QueryBuilder = MyQueryBuilder;
            public QueryBuilderType!: MyQueryBuilder<this, this[]>;
        };
    };
}

First thing that's important to note is that MyQueryBuilder cannot be declared at top level, because it depends on options that are passed as part of the mixin function. Well, this is fine, except if I want to generate declarations for this function, in which case I'm greeted with

Return type of exported function has or is using private name 'MyQueryBuilder'.ts(4060)

This error is basically telling me that I must export MyQueryBuilder to be able to generate declarations for it. Makes sense, but this is extremely hard (IMO). I was able to conjure something like this, which seems to work:

type AnyConstructor<A = object> = new (...input: any[]) => A;

interface MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
    ArrayQueryBuilderType: CursorQueryBuilder<M, M[]>;
    SingleQueryBuilderType: CursorQueryBuilder<M, M>;
    NumberQueryBuilderType: CursorQueryBuilder<M, number>;
    PageQueryBuilderType: CursorQueryBuilder<M, Page<M>>;
    session(): this;
}

interface MyModelInstance<M extends Model> {
    QueryBuilderType: MyQueryBuilder<this & M, this[]>;
}

interface MyModelStatic<M extends Model> {
    QueryBuilder: any; // Not sure what this does
    new (...args: any[]): MyModelInstance<M> & M;
}

export function Mixin(options: {sessionKey: string}) {
    return function <T extends AnyConstructor<Model>>(Base: T): MyModelStatic<InstanceType<T>> & T {
        class MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
            public ArrayQueryBuilderType!: MyQueryBuilder<M, M[]>;
            public SingleQueryBuilderType!: MyQueryBuilder<M, M>;
            public NumberQueryBuilderType!: MyQueryBuilder<M, number>;
            public PageQueryBuilderType!: MyQueryBuilder<M, Page<M>>;

            public session(session: any) {
                return this.mergeContext({
                    [options.sessionKey]: session
                });
            }
        }

        class MyModel extends Base {
            public static QueryBuilder = MyQueryBuilder;
            public QueryBuilderType!: MyQueryBuilder<this, this[]>;
        }

        return MyModel as MyModelStatic<InstanceType<T>> & T;
    };
}

With my limited testing, everything seems to be working as expected as far as typings go (TS v3.7.3).

The TS rewrite is in the ts-rewrite branch if you want to test stuff (please do) before I get as far as making a release.

olavim commented 4 years ago

I was able to extract MyQueryBuilder to top level, and simplify somewhat, by setting options as a static property of MyModel:

class MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
    public ArrayQueryBuilderType!: MyQueryBuilder<M, M[]>;
    public SingleQueryBuilderType!: MyQueryBuilder<M, M>;
    public NumberQueryBuilderType!: MyQueryBuilder<M, number>;
    public PageQueryBuilderType!: MyQueryBuilder<M, Page<M>>;

    public session(session: any) {
        return this.mergeContext({
            [this.options().sessionKey]: session
        });
    }

    private options() {
        const cls = this.modelClass() as any;
        return cls.options as Options;
    }
}

type ModelConstructor = new (...input: any[]) => Model;

interface MyModelInstance<T extends ModelConstructor > {
    /* MyQueryBuilder<this, this[]> would be optimal, but not possible since `this`
     * doesn't extend Model. Not sure what this affects either.
     */
    QueryBuilderType: MyQueryBuilder<this & InstanceType<T>, this[]>;
}

interface MyModel<T extends ModelConstructor> {
    options: Options;
    QueryBuilder: typeof CursorQueryBuilder;
    new (...args: any[]): MyModelInstance<T> & T;
}

export type MyMixin<T extends ModelConstructor> = MyModel<T> & T;

export function mixin(options: {sessionKey: string}) {
    return function <T extends ModelConstructor>(Base: T) {
        return class extends Base {
            // This let's us move MyQueryBuilder outside the function
            public static options = options;
            public static QueryBuilder = MyQueryBuilder;
            public QueryBuilderType!: MyQueryBuilder<this, this[]>;
        } as MyMixin<T>;
    };
}

I'm still required to write explicit types for MyModel. Type inference doesn't really work properly.

olavim commented 4 years ago

To me it seems like typings are still impossible to write. It's close, but breaks down once you use multiple plugins that modify the query builder.