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

Outer join nullable #11

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prettierrc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tabWidth = 4
printWidth = 220
1 change: 1 addition & 0 deletions src/IsNullable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type IsNullable<T> = null extends T ? true : never;
1 change: 1 addition & 0 deletions src/NonForeignKeyObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type NonForeignKeyObjects = any[] | Date;
5 changes: 5 additions & 0 deletions src/NonNullableRecursive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NonForeignKeyObjects } from './NonForeignKeyObjects';
// Removes all optional, undefined and null from type
export type NonNullableRecursive<T> = {
[P in keyof T]-?: T[P] extends object ? T[P] extends NonForeignKeyObjects ? Required<NonNullable<T[P]>> : NonNullableRecursive<T[P]> : Required<NonNullable<T[P]>>;
};
60 changes: 60 additions & 0 deletions src/TransformPropertiesToFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { IsNullable } from './IsNullable';
import { NonForeignKeyObjects } from './NonForeignKeyObjects';

// Gets the name of the property
type GetName<T> = T extends { name: infer X } ? X : never;
type GetNullIfNullable<T> = T extends { nullable: never } ? never : null;
// Add an object to an already existing array
type AddToArray<T extends { name: string }[], A extends any> = ((a: A, ...t: T) => void) extends ((...u: infer U) => void) ? U : never;



// Take { a : string, b : { c : string }} and return { a : ()=> {a: string}, b : { c : ()=> { b: { c: string } }}}
// PropertyPath contains the path to one leaf. {a : { b: string }} will have a PropertyPath of [{name:'b'}, {name:'a'}]
// Special cases:
// { a: User, b : string[] } returns { a: () => User, b : () => { b: string[] } }
// { a?: string[] } returns { a : () => (string[] | null) }
// { a: (User | null) } returns { a : () => User } (because you want to navigate the foreign keys when SELECT-ing)
export type TransformPropertiesToFunction<Model, PropertyPath extends {
name: any;
nullable?: true;
}[] = []> = {
[P in keyof Model]-?:
// if it's an object
Model[P] extends (object | undefined | null) ?
// and if it isn't a foreign key
Model[P] extends (NonForeignKeyObjects | undefined) ?
// create leaf with this type
() => RecordFromArray<AddToArray<PropertyPath, {
name: P;
}>, ({} extends {
[P2 in P]: Model[P];
} ? NonNullable<Model[P]> | null : Model[P])> :
// if it is a foreign key, transform it's properties
NonNullable<TransformPropertiesToFunction<Model[P], AddToArray<PropertyPath, {
name: P;
nullable: IsNullable<Model[P]>;
}>>> :
// if it isn't an object, create leaf with this type
() => RecordFromArray<AddToArray<PropertyPath, {
name: P;
}>, ({} extends {
[P2 in P]: Model[P];
} ? NonNullable<Model[P]> | null : Model[P])>;
};


// Creates a type from an array of strings (in reversed order)
// input [{ name: 'c'} ,{name: 'b'},{ name:'a'}] and string returns { a : { b: { c : string }}}
type RecordFromArray<Keys extends { name: any }[], LeafType> =
// Keys extends { 6: any } ? Record<Keys[6], Record<Keys[5], Record<Keys[4], Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>>>>> :
// Keys extends { 5: any } ? Record<Keys[5], Record<Keys[4], Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>>>> :
// Keys extends { 4: any } ? Record<Keys[4], Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>>> :
Keys extends { 3: any } ? Record<GetName<Keys[3]>, GetNullIfNullable<Keys[3]> | Record<GetName<Keys[2]>, GetNullIfNullable<Keys[2]> | Record<GetName<Keys[1]>, GetNullIfNullable<Keys[1]> | Record<GetName<Keys[0]>, LeafType>>>> :
Keys extends { 2: any } ? Record<GetName<Keys[2]>, GetNullIfNullable<Keys[2]> | Record<GetName<Keys[1]>, GetNullIfNullable<Keys[1]> | Record<GetName<Keys[0]>, LeafType>>> :
Keys extends { 1: any } ? Record<GetName<Keys[1]>, GetNullIfNullable<Keys[1]> | Record<GetName<Keys[0]>, LeafType>> :
Keys extends { 0: any } ? Record<GetName<Keys[0]>, LeafType> :
never;



78 changes: 39 additions & 39 deletions src/typedKnex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
getPrimaryKeyColumn,
getTableMetadata
} from './decorators';
import { NonForeignKeyObjects } from './NonForeignKeyObjects';
import { NonNullableRecursive } from './NonNullableRecursive';
import { TransformPropertiesToFunction } from './TransformPropertiesToFunction';

export function unflatten(o: any): any {
if (o instanceof Array) {
Expand Down Expand Up @@ -134,7 +137,7 @@ export interface ITypedQueryBuilder<Model, SelectableModel, Row> {

orderBy: IOrderBy<Model, SelectableModel, Row>;
innerJoinColumn: IKeyFunctionAsParametersReturnQueryBuider<Model, SelectableModel, Row>;
leftOuterJoinColumn: IKeyFunctionAsParametersReturnQueryBuider<Model, SelectableModel, Row>;
leftOuterJoinColumn: IOuterJoin<Model, SelectableModel, Row>;

whereColumn: IWhereCompareTwoColumns<Model, SelectableModel, Row>;

Expand All @@ -143,7 +146,7 @@ export interface ITypedQueryBuilder<Model, SelectableModel, Row> {
orWhereNull: IColumnParameterNoRowTransformation<Model, SelectableModel, Row>;
orWhereNotNull: IColumnParameterNoRowTransformation<Model, SelectableModel, Row>;

leftOuterJoinTableOnFunction: IJoinTableMultipleOnClauses<
leftOuterJoinTableOnFunction: IOuterJoinTableMultipleOnClauses<
Model,
SelectableModel,
Row extends Model ? {} : Row
Expand Down Expand Up @@ -379,6 +382,40 @@ interface IJoinTableMultipleOnClauses<Model, _SelectableModel, Row> {
>;
}

interface IOuterJoinTableMultipleOnClauses<Model, _SelectableModel, Row> {
<
NewPropertyType,
NewPropertyKey extends keyof any
>(
newPropertyKey: NewPropertyKey,
newPropertyClass: new () => NewPropertyType,
on: (
join: IJoinOnClause2<
AddPropertyWithType<Model, NewPropertyKey, NewPropertyType>,
NewPropertyType
>
) => void
): ITypedQueryBuilder<
Model,
Copy link

@beem812 beem812 Feb 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit for clarity:

On this and on other joins, this return type overrides any previous joins. By that I mean if I have a table A, and I join table B and then I join table C, when I go to select the columns I want I only have access to columns from table A and table C. This means if I want columns from all three tables I have to join to table B, select columns from it then join to table C and select columns from it. if the return type was ITypedQueryBuilder<Model, AddPropertyWithType<_SelectableModel, NewPropertyKey, NewPropertyType | null>, Row> all joined tables would be selectable when you got to choose what columns you want.

AddPropertyWithType<Model, NewPropertyKey, NewPropertyType | null>,
Row
>;
}



interface IOuterJoin<Model, SelectableModel, Row> {
(
selectColumnFunction: (
c: TransformPropertiesToFunction<Model>
) => void
): ITypedQueryBuilder<Model, SelectableModel, Row>;


}



interface ISelectRaw<Model, SelectableModel, Row> {
<
TReturn extends Boolean | String | Number,
Expand Down Expand Up @@ -432,10 +469,6 @@ type TransformPropsToFunctionsReturnPropertyName<Model> = {
() => P
};

type NonForeignKeyObjects = any[] | Date;



type TransformPropsToFunctionsReturnPropertyType<Model> = {
[P in keyof Model]:
Model[P] extends object ?
Expand Down Expand Up @@ -471,33 +504,6 @@ interface IDbFunctionWithAlias<Model, SelectableModel, Row> {
}


type NonNullableRecursive<T> = { [P in keyof T]-?: T[P] extends object ? T[P] extends NonForeignKeyObjects ? Required<NonNullable<T[P]>> : NonNullableRecursive<T[P]> : Required<NonNullable<T[P]>> };

type TransformPropertiesToFunction<Model, Result extends any[] = []> = {
[P in keyof Model]-?: Model[P] extends (object | undefined) ?
Model[P] extends (NonForeignKeyObjects | undefined) ? () => RecordFromArray<AddToArray<Result, P>, ({} extends { [P2 in P]: Model[P] } ? NonNullable<Model[P]> | null : Model[P])> :
TransformPropertiesToFunction<Model[P], AddToArray<Result, P>>
:
() => RecordFromArray<AddToArray<Result, P>, ({} extends { [P2 in P]: Model[P] } ? NonNullable<Model[P]> | null : Model[P])>
};

type RecordFromArray<Keys extends any[], LeafType> =
Keys extends { 6: any } ? Record<Keys[6], Record<Keys[5], Record<Keys[4], Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>>>>> :
Keys extends { 5: any } ? Record<Keys[5], Record<Keys[4], Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>>>> :
Keys extends { 4: any } ? Record<Keys[4], Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>>> :
Keys extends { 3: any } ? Record<Keys[3], Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>>> :
Keys extends { 2: any } ? Record<Keys[2], Record<Keys[1], Record<Keys[0], LeafType>>> :
Keys extends { 1: any } ? Record<Keys[1], Record<Keys[0], LeafType>> :
Keys extends { 0: any } ? Record<Keys[0], LeafType> :
never;





type AddToArray<T extends string[], A extends any> = ((a: A, ...t: T) => void) extends ((...u: infer U) => void) ? U : never;



interface ISelectWithFunctionColumns3<Model, SelectableModel, Row> {
<
Expand Down Expand Up @@ -715,12 +721,6 @@ interface IKeyFunctionAsParametersReturnQueryBuider<Model, SelectableModel, Row>
) => void
): ITypedQueryBuilder<Model, SelectableModel, Row>;

(
selectColumnFunction: (
c: TransformPropertiesToFunction<NonNullableRecursive<Model>>
) => void,
setToNullIfNullFunction: (r: Row) => void
): ITypedQueryBuilder<Model, SelectableModel, Row>;
}

interface IWhere<Model, SelectableModel, Row> {
Expand Down
39 changes: 38 additions & 1 deletion test/compilation/compilationTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ describe('compile time typed-knex', function() {

if (item !== undefined) {
console.log(item.user2.numericValue);
console.log(item.otherUser.name);
console.log(item.otherUser?.name);
}

})();
Expand All @@ -537,6 +537,43 @@ describe('compile time typed-knex', function() {
done();
});


it('should fail leftOuterJoinTableOnFunction result if it is used as not null', done => {
file = project.createSourceFile(
'test/test4.ts',
`
import * as knex from 'knex';
import { TypedKnex } from '../src/typedKnex';
import { User, UserSetting } from './testEntities';


(async () => {

const typedKnex = new TypedKnex(knex({ client: 'postgresql' }));

const item = await typedKnex
.query(UserSetting)
.leftOuterJoinTableOnFunction('otherUser', User, join => {
join.onColumns(i => i.user2Id, '=', j => j.id);
})
.select(i => [i.otherUser.name, i.user2.numericValue])
.getFirst();

if (item !== undefined) {
console.log(item.user2.numericValue);
console.log(item.otherUser.name);
}

})();
`
);

assert.equal(project.getPreEmitDiagnostics().length, 1);

file.delete();
done();
});

it('should not return type from leftOuterJoinTableOnFunction with not selected from joined table', done => {
file = project.createSourceFile(
'test/test4.ts',
Expand Down
2 changes: 2 additions & 0 deletions test/testEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ export class UserSetting {
public value!: string;
@Column()
public initialValue!: string;
@Column({ name: 'user3Id' })
public user3?: User;
}
70 changes: 58 additions & 12 deletions test/unit/typedQueryBuilderTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,24 +1339,70 @@ describe('TypedKnexQueryBuilder', () => {
});

// it('should stay commented out', async done => {

// const typedKnex = new TypedKnex(knex({ client: 'postgresql' }));
// const query = typedKnex
// .query(UserSetting)
// .innerJoinTableOnFunction('otherUser', User, join => {
// join.onColumns(i => i.user2Id, '=', j => j.id);
// join.onNull(i => i.name);
// })
// .select(i => i.otherUser.birthDate);

// // const item = await typedKnex
// // .query(UserSetting)
// // .insertItem({ id: '1', key: });
// const a = await query.getFirst();
// console.log('a: ', a.otherUser.birthDate);
// console.log('a: ', a.otherUser?.birthDate);

// done();
// });

// it('should stay commented out', async done => {

// const typedKnex = new TypedKnex(knex({ client: 'postgresql' }));
// const query = typedKnex
// .query(UserSetting)
// .leftOuterJoinTableOnFunction('otherUser', User, join => {
// join.onColumns(i => i.user2Id, '=', j => j.id);
// join.onNull(i => i.name);
// })
// .select(i => i.otherUser.birthDate);

// const a = await query.getFirst();
// console.log('a: ', a.otherUser.birthDate);
// console.log('a: ', a.otherUser?.birthDate);

// done();
// })

// const item = await typedKnex
// .query(User)
// .select(i => i.category.name)
// .getFirst();

// console.log('item: ', item.category.name);
// it('should stay commented out', async done => {

// const typedKnex = new TypedKnex(knex({ client: 'postgresql' }));
// const query = typedKnex
// .query(UserSetting)
// .innerJoinColumn(i => i.user3)
// .select(i => i.user3.birthDate);

// const a = await query.getFirst();
// console.log('a: ', a.user3.birthDate);
// console.log('a: ', a.user3?.birthDate);

// done();
// });

// // if (item !== undefined) {
// // console.log(item.user2.numericValue);
// // console.log(item.otherUser.name);
// // }
// it('should stay commented out', async done => {

// const typedKnex = new TypedKnex(knex({ client: 'postgresql' }));
// const query = typedKnex
// .query(UserSetting)
// .leftOuterJoinColumn(i => i.user3)
// .select(i => i.user3.birthDate);

// const a = await query.getFirst();
// console.log('a: ', a.user3.birthDate);
// console.log('a: ', a.user3?.birthDate);

// done();
// });

});