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

Implement first level typing of withGraphFetched (non recursive) #6

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
20,281 changes: 31 additions & 20,250 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"pg": "^8.7.1",
"prettier": "3.0.3",
"sqlite3": "^5.0.2",
"typescript": ">=4.5.4",
"typescript": "^5.5.1-rc",
"vuepress": "1.9.10"
}
}
36 changes: 35 additions & 1 deletion tests/ts/custom-query-builder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import { Model, QueryBuilder, Page, TransactionOrKnex } from '../../';
import { Model, QueryBuilder, Page, TransactionOrKnex, ObjectRelationExpression, GraphOptions, OnlyKeysFromModel, RestrictType, SetRequired, StringRelationExpression } from '../../';


interface GraphFetchedMethod<M extends Model> {
<Expr extends ObjectRelationExpression<M>>(
expr: RestrictType<Expr, ObjectRelationExpression<M>>,
options?: GraphOptions,
): CustomQueryBuilder<
// Model is here to satisfy the Model requirement in the CustomQueryBuilder type
// sometimes creates errors because of recursivity
Model & SetRequired<M, Expr>
>;
(expr: StringRelationExpression<M>, options?: GraphOptions): CustomQueryBuilder<M>;
}

// class customCustomQueryBuilder<M extends Model, R = M[]> extends CustomQueryBuilder<M, R> {
// <Expr extends ObjectRelationExpression<M>>(
// expr: RestrictType<Expr, ObjectRelationExpression<M>>,
// options?: GraphOptions,
// ): CustomQueryBuilder<
// // Model is here to satisfy the Model requirement in the CustomQueryBuilder type
// // sometimes creates errors because of recursivity
// Model & SetRequired<M, Expr>
// >;
// (expr: StringRelationExpression<M>, options?: GraphOptions): CustomQueryBuilder<M>;
// }
class CustomQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
ArrayQueryBuilderType!: CustomQueryBuilder<M, M[]>;
SingleQueryBuilderType!: CustomQueryBuilder<M, M>;
MaybeSingleQueryBuilderType!: CustomQueryBuilder<M, M | undefined>;
NumberQueryBuilderType!: CustomQueryBuilder<M, number>;
PageQueryBuilderType!: CustomQueryBuilder<M, Page<M>>;
GraphFetchedQueryBuilderType!: GraphFetchedMethod<M>;

someCustomMethod(): this {
return this;
Expand Down Expand Up @@ -57,3 +82,12 @@ const numUpdated: CustomQueryBuilder<Person, number> = Person.query()
const allPets: PromiseLike<Animal[]> = Person.relatedQuery('pets')
.for(Person.query().select('id'))
.someCustomMethod();


(async () => {
const testToJson = await Person.query().withGraphFetched({ pets: true });
testToJson[0].toJSON();
const testToJsonFirst = await Person.query().withGraphFetched({ pets: true }).first();
// const testToJsonFirst = await Person.query().withGraphFetched('pets').first();
testToJsonFirst?.toJSON();
})
4 changes: 2 additions & 2 deletions tests/ts/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ const rowsEager: PromiseLike<Person[]> = Person.query().withGraphFetched('foo.ba
joinOperation: 'innerJoin',
});

const rowsEager2: PromiseLike<Person[]> = Person.query().withGraphFetched({
const rowsEager2 = Person.query().withGraphFetched({
pets: {
owner: {
movies: {
Expand All @@ -712,7 +712,7 @@ const rowsEager2: PromiseLike<Person[]> = Person.query().withGraphFetched({
});

const rowsEager3: PromiseLike<Person[]> = Person.query().withGraphFetched({
foo: {
mom: {
bar: true,
},
});
Expand Down
1 change: 1 addition & 0 deletions tests/ts/fixtures/person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class Person extends objection.Model {
children?: Person[];
// Note that $relatedQuery won't work for optional fields (at least until TS 2.8), so this gets a !:
pets!: Animal[];
pet?: Animal;
comments?: Comment[];
movies?: Movie[];
age!: number;
Expand Down
48 changes: 48 additions & 0 deletions tests/ts/model/instance-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,53 @@ const takesPersonPojo = (person: ModelObject<Person>) => true;

const person = Person.fromJson({ firstName: 'Jennifer' });
const personPojo = person.toJSON();
// @ts-expect-error toJSON should remove the base Model methods recursively even on arrays
personPojo.children![0].$query

(async () => {
const pq = Person.query().withGraphFetched({
mom: {
mom: true
},
pet: true
});

const p = await pq;
pq.ModelType.pet;
if(p) {
const pj = p[0].toJSON();
// test that toJSON keeps the required properties
pj.pet.id;
pj.mom;
// @ts-expect-error test that toJSON works
pj.movies.at(0);
pj.mom.mom.children?.at(0)?.lastName;
// @ts-expect-error test that toJSON works
pj.$fetchGraph;
// @ts-expect-error test that toJSON works recursively
pj.mom.$fetchGraph;
}

});

(async () => {
const p = await Person.query().withGraphFetched({
mom: {
mom: true
},
pet: true
}).whereIn('id', [12,11,14]).ignore().first();

if(p) {
//const pj = p[0].blurb();
// test that toJSON keeps the required properties
p.pet.id;
p.mom;
// @ts-expect-error test that toJSON works
p.movies.at(0);
p.mom.mom.children?.at(0)?.lastName;
}

});

takesPersonPojo(personPojo);
79 changes: 79 additions & 0 deletions tests/ts/query-examples/eager-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,69 @@ import { Person } from '../fixtures/person';
},
});

// @ts-expect-error property foo does not exist
await Person.query().withGraphFetched({
foo: true,
children: {
pets: true,
children: true,
},
});

const personWithPets = await Person.query().withGraphFetched({
pet: true,
children: {
pets: true,
children: true,
},
}).first();

if(personWithPets) {
personWithPets.pet;
personWithPets.pet.name;
// @ts-expect-error mom was not fetched
personWithPets.mom.lastName;
personWithPets.children;
personWithPets.children.at(0);
personWithPets.children.at(0)?.children;
personWithPets.children.at(0)?.children.at(0);
}

const personWithMom = await Person.query().withGraphFetched({
mom: {
mom: true,
children: true,
},
}).first();
if(personWithMom){
personWithMom.mom.lastName;
personWithMom.mom.mom.lastName;
// @ts-expect-error pet was not fetched
personWithMom.mom.mom.pet.name;
// @ts-expect-error mom was not fetched
personWithMom.mom.mom.mom.lastName;
}

const personAlone = await Person.query().first();
if(personAlone){
// @ts-expect-error we didnt fetch pet
personAlone.pet.name;
}

const personWitModifier = await Person.query().withGraphFetched({
mom: {
$modify: ['selectAll'],
mom: {
pet: true,
}
},
}).first();
if(personWitModifier){
personWitModifier.mom.mom.pet.name;
// @ts-expect-error
personWitModifier.mom['$modify'];
}

await Person.query().withGraphFetched('[pets, children.^]');

await Person.query().withGraphFetched('[pets, children.^3]');
Expand Down Expand Up @@ -63,4 +126,20 @@ import { Person } from '../fixtures/person';
await Person.query().withGraphFetched('[pets, children.pets]');

await Person.query().withGraphJoined('[pets, children.pets]');

const withRelations = await Person.query().withGraphFetched({
pets: true,
children: {
pets: true,
children: true,
},
});

// no error as we ask for children in the withGraphFetched
withRelations[0].children[0];

// should have an error as we didn't ask for comments in the withGraphFetched
// @ts-expect-error
withRelations[0].comments[0]

})();
90 changes: 87 additions & 3 deletions typings/objection/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IsEqual, Simplify } from 'type-fest';
/// <reference types="node" />

// Type definitions for Objection.js
Expand All @@ -15,7 +16,7 @@
import Ajv, { Options as AjvOptions } from 'ajv';
import * as dbErrors from 'db-errors';
import { Knex } from 'knex';
import { SnakeCase, SnakeCasedProperties } from 'type-fest';
import { SnakeCase } from 'type-fest';

// Export the entire Objection namespace.
export = Objection;
Expand Down Expand Up @@ -171,6 +172,18 @@ declare namespace Objection {
}

type RelationExpression<M extends Model> = string | object;
type StringRelationExpression<M extends Model> = string;
type ObjectRelationExpression<M extends Model> = {readonly [key in keyof ModelObject<M>]?: object | boolean};
type ExcludeUndefinedKeys<T> = T extends object
? {
[K in keyof T as undefined extends T[K] ? never : K]: ExcludeUndefinedKeys<T[K]>;
}
: T;

// keeps keys starting with the $ sign and toJSON and QueryBuilderType
type OnlyKeysFromModel<M extends Model> = {
[K in keyof M as K extends `$${string}` ? K : never]: M[K];
} //& {toJSON: M['toJSON'], QueryBuilderType: M['QueryBuilderType']};

/**
* If T is an array, returns the item type, otherwise returns T.
Expand All @@ -192,10 +205,28 @@ declare namespace Objection {
*/
type Defined<T> = Exclude<T, undefined>;


/**
* Filter out keys from an object.
*/
type Filter<KeyType, ExcludeType> = IsEqual<KeyType, ExcludeType> extends true ? never : (KeyType extends ExcludeType ? never : KeyType);

/**
* Does an Except recursively, removing the keys of the ExclType on each level of ObjectType if the property of ObjectType extends the ExclType.
*/
type ExceptTypeDeep<ObjectType extends ExclType, ExclType> = {
[KeyType in keyof ObjectType as Filter<KeyType, keyof ExclType>]:
ObjectType[KeyType] extends ExclType ?
ExceptTypeDeep<ObjectType[KeyType], ExclType>
: Defined<ObjectType[KeyType]> extends Array<infer ArrayItem> ?
ArrayItem extends ExclType ? Array<ExceptTypeDeep<ArrayItem, ExclType>> : ObjectType[KeyType] : ObjectType[KeyType];
};


/**
* A Pojo version of model.
*/
type ModelObject<T extends Model> = Pick<T, DataPropertyNames<T>>;
type ModelObject<T extends Model> = ExceptTypeDeep<T, Model>;

/**
* Any object that has some of the properties of model class T match this type.
Expand All @@ -210,6 +241,28 @@ declare namespace Objection {
: Expression<T[K]>;
};

/**
* Restrict the keys to the ones present in Restriction
*/
type RestrictType<TypeToRestrict, Restriction> = { [K in keyof TypeToRestrict]: K extends keyof Restriction ? TypeToRestrict[K] : never }

type ModelKeys = Simplify<keyof Model>;
/**
* Recursive SetRequired
*/
type SetRequired<T extends Model, Required> = // Omit<T, Exclude<keyof Required, keyof Model>> &
Omit<T, keyof Required> &
{[k in keyof T as k extends keyof Required ? k : never]-?:
k extends keyof Required ?
Required[k] extends object ?
NonNullable<T[k]> extends Array<infer ItemType extends Model> ?
Array<Simplify<SetRequired<ItemType, Required[k]>>>
: T[k] extends Model ? Simplify<SetRequired<NonNullable<T[k]>, Required[k]>> : T[k]
: T[k]
: T[k]
}
& Pick<T, ModelKeys> ;

/**
* Additional optional parameters that may be used in graphs.
*/
Expand Down Expand Up @@ -336,6 +389,11 @@ declare namespace Objection {
*/
type PageQueryBuilder<T extends { PageQueryBuilderType: any }> = T['PageQueryBuilderType'];

/**
* Gets the page query builder type for a query builder.
*/
type GraphFetchedHack<T extends { GraphFetchedQueryBuilderType: any }> = T['GraphFetchedQueryBuilderType'];

interface ForClassMethod {
<M extends Model>(modelClass: ModelConstructor<M>): QueryBuilderType<M>;
}
Expand Down Expand Up @@ -1085,7 +1143,18 @@ declare namespace Objection {
unrelate(): NumberQueryBuilder<this>;
for(ids: ForIdValue | ForIdValue[]): this;

withGraphFetched(expr: RelationExpression<M>, options?: GraphOptions): this;
// withGraphFetched(expr: StringRelationExpression<M>, options?: GraphOptions): this;
// withGraphFetched<Expr extends ObjectRelationExpression<M>>(
// expr: RestrictType<Expr, ObjectRelationExpression<M>>,
// options?: GraphOptions
// ): QueryBuilder<Model & SetRequired<M, Expr>>;
// withGraphFetched<Expr extends ObjectRelationExpression<M>>(
// expr: RestrictType<Expr, ObjectRelationExpression<M>>,
// options?: GraphOptions
// ): QueryBuilder<M & SetRequired<M, Expr>>; // Model is here to guarantee that we have '$modelClass', '$relatedQuery', '$query' etc. as they will never be in 'required'
withGraphFetched: GraphFetchedHack<this>;
// withGraphFetched: GraphFetchedMethod<M>;
// withGraphFetched(expr: StringRelationExpression<M>, options?: GraphOptions): this;
withGraphJoined(expr: RelationExpression<M>, options?: GraphOptions): this;

truncate(): Promise<void>;
Expand Down Expand Up @@ -1180,6 +1249,7 @@ declare namespace Objection {
MaybeSingleQueryBuilderType: QueryBuilder<M, M | undefined>;
NumberQueryBuilderType: QueryBuilder<M, number>;
PageQueryBuilderType: QueryBuilder<M, Page<M>>;
GraphFetchedQueryBuilderType: GraphFetchedMethod<M>;

then<R1 = R, R2 = never>(
onfulfilled?: ((value: R) => R1 | PromiseLike<R1>) | undefined | null,
Expand All @@ -1191,6 +1261,20 @@ declare namespace Objection {
): Promise<R | FR>;
}

interface GraphFetchedMethod<M extends Model> {
<Expr extends ObjectRelationExpression<M>>(
expr: RestrictType<Expr, ObjectRelationExpression<M>>,
options?: GraphOptions
): QueryBuilder<
// Model is here to satisfy the Model requirement in the QueryBuilder type
// sometimes creates errors because of recursivity
// Omit<Model, 'QueryBuilderType'> & { QueryBuilderType: QueryBuilder<M>} &
SetRequired<M, Expr>
//& { toJSON(opt?: ToJsonOptions): ModelObject<SetRequired<M, Expr>>}
> ;
(expr: StringRelationExpression<M>, options?: GraphOptions): QueryBuilder<M>;
}

type X<T> = Promise<T>;

interface FetchGraphOptions {
Expand Down