// tslint:disable:use-named-parameter import { Knex } from 'knex'; import { getColumnInformation, getColumnProperties, getPrimaryKeyColumn, getTableMetadata } from './decorators'; import { mapObjectToTableObject } from './mapObjectToTableObject'; import { NestedForeignKeyKeysOf, NestedKeysOf } from './NestedKeysOf'; import { NonForeignKeyObjects } from './NonForeignKeyObjects'; import { NonNullableRecursive } from './NonNullableRecursive'; import { GetNestedProperty, GetNestedPropertyType } from './PropertyTypes'; import { SelectableColumnTypes } from './SelectableColumnTypes'; import { FlattenOption, setToNull, unflatten } from './unflatten'; export class ModelRepository { constructor(private knex: Knex) { } public query(tableClass: new () => T): ITypedQueryBuilder { return new TypedQueryBuilder(tableClass, this.knex); } public beginTransaction(): Promise { return new Promise((resolve) => { this.knex .transaction((tr) => resolve(tr)) // If this error is not caught here, it will throw, resulting in an unhandledRejection // tslint:disable-next-line:no-empty .catch((_e) => {}); }); } } let beforeInsertTransform = undefined as undefined | ((item: any, typedQueryBuilder: any) => any); export function registerBeforeInsertTransform(f: (item: T, typedQueryBuilder: ITypedQueryBuilder<{}, {}, {}>) => T) { beforeInsertTransform = f; } let beforeUpdateTransform = undefined as undefined | ((item: any, typedQueryBuilder: any) => any); export function registerBeforeUpdateTransform(f: (item: T, typedQueryBuilder: ITypedQueryBuilder<{}, {}, {}>) => T) { beforeUpdateTransform = f; } class NotImplementedError extends Error { constructor() { super('Not implemented'); } } export interface ITypedQueryBuilder { columns: { name: string }[]; where: IWhereWithOperator; andWhere: IWhereWithOperator; orWhere: IWhereWithOperator; whereNot: IWhere; select: ISelectWithFunctionColumns3; selectQuery: ISelectQuery; orderBy: IOrderBy; innerJoinColumn: IKeyFunctionAsParametersReturnQueryBuider; leftOuterJoinColumn: IKeyFunctionAsParametersReturnQueryBuider; whereColumn: IWhereCompareTwoColumns; whereNull: IColumnParameterNoRowTransformation; whereNotNull: IColumnParameterNoRowTransformation; orWhereNull: IColumnParameterNoRowTransformation; orWhereNotNull: IColumnParameterNoRowTransformation; leftOuterJoinTableOnFunction: IJoinTableMultipleOnClauses; innerJoinTableOnFunction: IJoinTableMultipleOnClauses; selectRaw: ISelectRaw; findByPrimaryKey: IFindByPrimaryKey; whereIn: IWhereIn; whereNotIn: IWhereIn; orWhereIn: IWhereIn; orWhereNotIn: IWhereIn; whereBetween: IWhereBetween; whereNotBetween: IWhereBetween; orWhereBetween: IWhereBetween; orWhereNotBetween: IWhereBetween; whereExists: IWhereExists; orWhereExists: IWhereExists; whereNotExists: IWhereExists; orWhereNotExists: IWhereExists; whereParentheses: IWhereParentheses; groupBy: ISelectableColumnKeyFunctionAsParametersReturnQueryBuider; having: IHaving; havingNull: ISelectableColumnKeyFunctionAsParametersReturnQueryBuider; havingNotNull: ISelectableColumnKeyFunctionAsParametersReturnQueryBuider; havingIn: IWhereIn; havingNotIn: IWhereIn; havingExists: IWhereExists; havingNotExists: IWhereExists; havingBetween: IWhereBetween; havingNotBetween: IWhereBetween; union: IUnion; unionAll: IUnion; min: IDbFunctionWithAlias; count: IDbFunctionWithAlias; countDistinct: IDbFunctionWithAlias; max: IDbFunctionWithAlias; sum: IDbFunctionWithAlias; sumDistinct: IDbFunctionWithAlias; avg: IDbFunctionWithAlias; avgDistinct: IDbFunctionWithAlias; insertSelect: IInsertSelect; clearSelect(): ITypedQueryBuilder; clearWhere(): ITypedQueryBuilder; clearOrder(): ITypedQueryBuilder; limit(value: number): ITypedQueryBuilder; offset(value: number): ITypedQueryBuilder; useKnexQueryBuilder(f: (query: Knex.QueryBuilder) => void): ITypedQueryBuilder; toQuery(): string; getFirstOrNull(flattenOption?: FlattenOption): Promise : Row | null>; getFirst(flattenOption?: FlattenOption): Promise : Row>; getSingleOrNull(flattenOption?: FlattenOption): Promise : Row | null>; getSingle(flattenOption?: FlattenOption): Promise : Row>; getMany(flattenOption?: FlattenOption): Promise<(Row extends Model ? RemoveObjectsFrom : Row)[]>; getCount(): Promise; insertItem(newObject: Partial>): Promise; insertItems(items: Partial>[]): Promise; del(): Promise; delByPrimaryKey(primaryKeyValue: any): Promise; updateItem(item: Partial>): Promise; updateItemByPrimaryKey(primaryKeyValue: any, item: Partial>): Promise; updateItemsByPrimaryKey( items: { primaryKeyValue: any; data: Partial>; }[] ): Promise; execute(): Promise; whereRaw(sql: string, ...bindings: string[]): ITypedQueryBuilder; havingRaw(sql: string, ...bindings: string[]): ITypedQueryBuilder; transacting(trx: Knex.Transaction): ITypedQueryBuilder; truncate(): Promise; distinct(): ITypedQueryBuilder; clone(): ITypedQueryBuilder; groupByRaw(sql: string, ...bindings: string[]): ITypedQueryBuilder; orderByRaw(sql: string, ...bindings: string[]): ITypedQueryBuilder; keepFlat(): ITypedQueryBuilder; } type ReturnNonObjectsNamesOnly = { [K in keyof T]: T[K] extends SelectableColumnTypes ? K : never }[keyof T]; type RemoveObjectsFrom = { [P in ReturnNonObjectsNamesOnly]: T[P] }; export type ObjectToPrimitive = T extends String ? string : T extends Number ? number : T extends Boolean ? boolean : never; export type Operator = '=' | '!=' | '>' | '<' | string; interface IConstructor { new (...args: any[]): T; } export type AddPropertyWithType = Original & Record; interface IColumnParameterNoRowTransformation { , keyof NonNullableRecursive, ''>>(key: ConcatKey): ITypedQueryBuilder; } interface IJoinOn { , keyof NonNullableRecursive, ''>, ConcatKey2 extends NestedKeysOf, keyof NonNullableRecursive, ''>>( key1: ConcatKey1, operator: Operator, key2: ConcatKey2 ): IJoinOnClause2; } interface IJoinOnVal { , keyof NonNullableRecursive, ''>>(key: ConcatKey, operator: Operator, value: any): IJoinOnClause2; } interface IJoinOnNull { , keyof NonNullableRecursive, ''>>(key: ConcatKey): IJoinOnClause2; } interface IJoinOnClause2 { on: IJoinOn; orOn: IJoinOn; andOn: IJoinOn; onVal: IJoinOnVal; andOnVal: IJoinOnVal; orOnVal: IJoinOnVal; onNull: IJoinOnNull; } interface IInsertSelect { , keyof NonNullableRecursive, ''>>( newPropertyClass: new () => NewPropertyType, ...columnNames: ConcatKey[] ): Promise; } interface IJoinTableMultipleOnClauses { ( newPropertyKey: NewPropertyKey, newPropertyClass: new () => NewPropertyType, on: (join: IJoinOnClause2, NewPropertyType>) => void ): ITypedQueryBuilder, AddPropertyWithType, Row>; } interface ISelectRaw { (name: TName, returnType: IConstructor, query: string): ITypedQueryBuilder< Model, SelectableModel, Record> & Row >; } interface ISelectQuery { ( name: TName, returnType: IConstructor, subQueryModel: new () => SubQueryModel, code: (subQuery: ITypedQueryBuilder, parent: TransformPropsToFunctionsReturnPropertyName) => void ): ITypedQueryBuilder> & Row>; } type TransformPropsToFunctionsReturnPropertyName = { [P in keyof Model]: Model[P] extends object ? (Model[P] extends Required ? () => P : TransformPropsToFunctionsReturnPropertyName) : () => P; }; interface IOrderBy { , keyof NonNullableRecursive, ''>, TName extends keyof any>( columnNames: ConcatKey, direction?: 'asc' | 'desc' ): ITypedQueryBuilder>>; } interface IDbFunctionWithAlias { , keyof NonNullableRecursive, ''>, TName extends keyof any>(columnNames: ConcatKey, name: TName): ITypedQueryBuilder< Model, SelectableModel, Row & Record> >; } type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; interface ISelectWithFunctionColumns3 { , keyof NonNullableRecursive, ''>>(...columnNames: ConcatKey[]): ITypedQueryBuilder< Model, SelectableModel, Row & UnionToIntersection> >; } interface IFindByPrimaryKey<_Model, SelectableModel, Row> { , keyof NonNullableRecursive, ''>>(primaryKeyValue: any, ...columnNames: ConcatKey[]): Promise< (Row & UnionToIntersection>) | undefined >; } interface IKeyFunctionAsParametersReturnQueryBuider { , keyof NonNullableRecursive, ''>>(key: ConcatKey): ITypedQueryBuilder; } interface ISelectableColumnKeyFunctionAsParametersReturnQueryBuider { , keyof NonNullableRecursive, ''>>(key: ConcatKey): ITypedQueryBuilder; } interface IWhere { , keyof NonNullableRecursive, ''>>(key: ConcatKey, value: GetNestedPropertyType): ITypedQueryBuilder; } interface IWhereWithOperator { , keyof NonNullableRecursive, ''>>(key: ConcatKey, value: GetNestedPropertyType): ITypedQueryBuilder; , keyof NonNullableRecursive, ''>>(key: ConcatKey, operator: Operator, value: GetNestedPropertyType): ITypedQueryBuilder< Model, SelectableModel, Row >; } interface IWhereIn { , keyof NonNullableRecursive, ''>>(key: ConcatKey, value: GetNestedPropertyType[]): ITypedQueryBuilder; } interface IWhereBetween { , keyof NonNullableRecursive, ''>, PropertyType extends GetNestedPropertyType>( key: ConcatKey, value: [PropertyType, PropertyType] ): ITypedQueryBuilder; } interface IHaving { , keyof NonNullableRecursive, ''>>(key: ConcatKey, operator: Operator, value: GetNestedPropertyType): ITypedQueryBuilder< Model, SelectableModel, Row >; } interface IWhereCompareTwoColumns { <_PropertyType1, _PropertyType2, Model2>( key1: NestedKeysOf, keyof NonNullableRecursive, ''>, operator: Operator, key2: NestedKeysOf, keyof NonNullableRecursive, ''> ): ITypedQueryBuilder; } interface IWhereExists { ( subQueryModel: new () => SubQueryModel, code: (subQuery: ITypedQueryBuilder, parent: TransformPropsToFunctionsReturnPropertyName) => void ): ITypedQueryBuilder; } interface IWhereParentheses { (code: (subQuery: ITypedQueryBuilder) => void): ITypedQueryBuilder; } interface IUnion { (subQueryModel: new () => SubQueryModel, code: (subQuery: ITypedQueryBuilder) => void): ITypedQueryBuilder; } function getProxyAndMemories(typedQueryBuilder?: TypedQueryBuilder) { const memories = [] as string[]; function allGet(_target: any, name: any): any { if (name === 'memories') { return memories; } if (name === 'getColumnName') { return typedQueryBuilder!.getColumnName(...memories); } if (typeof name === 'string') { memories.push(name); } return new Proxy( {}, { get: allGet, } ); } const root = new Proxy( {}, { get: allGet, } ); return { root, memories }; } function getProxyAndMemoriesForArray(typedQueryBuilder?: TypedQueryBuilder) { const result = [] as string[][]; let counter = -1; function allGet(_target: any, name: any): any { if (_target.level === 0) { counter++; result.push([]); } if (name === 'memories') { return result[counter]; } if (name === 'result') { return result; } if (name === 'level') { return _target.level; } if (name === 'getColumnName') { return typedQueryBuilder!.getColumnName(...result[counter]); } if (typeof name === 'string') { result[counter].push(name); } return new Proxy( {}, { get: allGet, } ); } const root = new Proxy( { level: 0 }, { get: allGet, } ); return { root, result }; } class TypedQueryBuilder implements ITypedQueryBuilder { public columns: { name: string }[]; public onlyLogQuery = false; public queryLog = ''; private hasSelectClause = false; private queryBuilder: Knex.QueryBuilder; private tableName: string; private shouldUnflatten: boolean; private extraJoinedProperties: { name: string; propertyType: new () => any; }[]; private transaction?: Knex.Transaction; constructor(private tableClass: new () => ModelType, private knex: Knex, queryBuilder?: Knex.QueryBuilder, private parentTypedQueryBuilder?: any) { this.tableName = getTableMetadata(tableClass).tableName; this.columns = getColumnProperties(tableClass); if (queryBuilder !== undefined) { this.queryBuilder = queryBuilder; this.queryBuilder.from(this.tableName); } else { this.queryBuilder = this.knex.from(this.tableName); } this.extraJoinedProperties = []; this.shouldUnflatten = true; } public keepFlat() { this.shouldUnflatten = false; return this; } public async del() { await this.queryBuilder.del(); } public async delByPrimaryKey(value: any) { const primaryKeyColumnInfo = getPrimaryKeyColumn(this.tableClass); await this.queryBuilder.del().where(primaryKeyColumnInfo.name, value); } public async insertItem(newObject: Partial>) { await this.insertItems([newObject]); } public async insertItems(items: Partial>[]) { items = [...items]; for (let item of items) { if (beforeInsertTransform) { item = beforeInsertTransform(item, this); } } items = items.map((item) => mapObjectToTableObject(this.tableClass, item)); while (items.length > 0) { const chunk = items.splice(0, 500); const query = this.knex.from(this.tableName).insert(chunk); if (this.transaction !== undefined) { query.transacting(this.transaction); } if (this.onlyLogQuery) { this.queryLog += query.toQuery() + '\n'; } else { await query; } } } public async updateItem(item: Partial>) { if (beforeUpdateTransform) { item = beforeUpdateTransform(item, this); } const mappedItem = mapObjectToTableObject(this.tableClass, item); if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.update(mappedItem).toQuery() + '\n'; } else { await this.queryBuilder.update(mappedItem); } } public async updateItemByPrimaryKey(primaryKeyValue: any, item: Partial>) { if (beforeUpdateTransform) { item = beforeUpdateTransform(item, this); } const mappedItem = mapObjectToTableObject(this.tableClass, item); const primaryKeyColumnInfo = getPrimaryKeyColumn(this.tableClass); const query = this.queryBuilder.update(mappedItem).where(primaryKeyColumnInfo.name, primaryKeyValue); if (this.onlyLogQuery) { this.queryLog += query.toQuery() + '\n'; } else { await query; } } public async updateItemsByPrimaryKey( items: { primaryKeyValue: any; data: Partial>; }[] ) { const primaryKeyColumnInfo = getPrimaryKeyColumn(this.tableClass); items = [...items]; while (items.length > 0) { const chunk = items.splice(0, 500); let sql = ''; for (const item of chunk) { const query = this.knex.from(this.tableName); if (beforeUpdateTransform) { item.data = beforeUpdateTransform(item.data, this); } item.data = mapObjectToTableObject(this.tableClass, item.data); query.update(item.data); sql += query.where(primaryKeyColumnInfo.name, item.primaryKeyValue).toString().replace('?', '\\?') + ';\n'; } const finalQuery = this.knex.raw(sql); if (this.transaction !== undefined) { finalQuery.transacting(this.transaction); } if (this.onlyLogQuery) { this.queryLog += finalQuery.toQuery() + '\n'; } else { await finalQuery; } } } public async execute() { await this.queryBuilder; } public limit(value: number) { this.queryBuilder.limit(value); return this as any; } public offset(value: number) { this.queryBuilder.offset(value); return this as any; } public async findById(id: string, columns: (keyof ModelType)[]) { return await this.queryBuilder .select(columns as any) .where(this.tableName + '.id', id) .first(); } public async getCount() { const query = this.queryBuilder.count({ count: '*' }); const result = await query; if (result.length === 0) { return 0; } return result[0].count; } public async getFirstOrNull() { if (this.hasSelectClause === false) { this.selectAllModelProperties(); } if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.toQuery() + '\n'; return []; } else { const items = await this.queryBuilder; if (!items || items.length === 0) { return null; } return this.flattenByOption(items[0], arguments[0]); } } public async getFirst() { if (this.hasSelectClause === false) { this.selectAllModelProperties(); } if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.toQuery() + '\n'; return []; } else { const items = await this.queryBuilder; if (!items || items.length === 0) { throw new Error('Item not found.'); } return this.flattenByOption(items[0], arguments[0]); } } public async getSingleOrNull() { if (this.hasSelectClause === false) { this.selectAllModelProperties(); } if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.toQuery() + '\n'; return []; } else { const items = await this.queryBuilder; if (!items || items.length === 0) { return null; } else if (items.length > 1) { throw new Error(`More than one item found: ${items.length}.`); } return this.flattenByOption(items[0], arguments[0]); } } public async getSingle() { if (this.hasSelectClause === false) { this.selectAllModelProperties(); } if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.toQuery() + '\n'; return []; } else { const items = await this.queryBuilder; if (!items || items.length === 0) { throw new Error('Item not found.'); } else if (items.length > 1) { throw new Error(`More than one item found: ${items.length}.`); } return this.flattenByOption(items[0], arguments[0]); } } public selectColumn() { this.hasSelectClause = true; let calledArguments = [] as string[]; function saveArguments(...args: string[]) { calledArguments = args; } arguments[0](saveArguments); this.queryBuilder.select(this.getColumnName(...calledArguments) + ' as ' + this.getColumnSelectAlias(...calledArguments)); return this as any; } public getArgumentsFromColumnFunction3(f: any) { const { root, result } = getProxyAndMemoriesForArray(); f(root); return result; } public select2() { this.hasSelectClause = true; const f = arguments[0]; const columnArgumentsList = this.getArgumentsFromColumnFunction3(f); for (const columnArguments of columnArgumentsList) { this.queryBuilder.select(this.getColumnName(...columnArguments) + ' as ' + this.getColumnSelectAlias(...columnArguments)); } return this as any; } public select() { this.hasSelectClause = true; let columnArgumentsList: string[][]; if (typeof arguments[0] === 'string') { columnArgumentsList = [...arguments].map((concatKey: string) => concatKey.split('.')); } else { const f = arguments[0]; columnArgumentsList = this.getArgumentsFromColumnFunction3(f); } for (const columnArguments of columnArgumentsList) { this.queryBuilder.select(this.getColumnName(...columnArguments) + ' as ' + this.getColumnSelectAlias(...columnArguments)); } return this as any; } public orderBy() { this.queryBuilder.orderBy(this.getColumnNameWithoutAliasFromFunctionOrString(arguments[0]), arguments[1]); return this as any; } public async getMany(): Promise<(Row extends ModelType ? RemoveObjectsFrom : Row)[]> { if (this.hasSelectClause === false) { this.selectAllModelProperties(); } if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.toQuery() + '\n'; return []; } else { const items = await this.queryBuilder; return this.flattenByOption(items, arguments[0]) as (Row extends ModelType ? RemoveObjectsFrom : Row)[]; } } public selectRaw() { this.hasSelectClause = true; const name = arguments[0]; const query = arguments[2]; this.queryBuilder.select(this.knex.raw(`(${query}) as "${name}"`)); return this as any; } public innerJoinColumn() { return this.joinColumn('innerJoin', arguments[0]); } public leftOuterJoinColumn() { return this.joinColumn('leftOuterJoin', arguments[0]); } public innerJoinTable() { const newPropertyKey = arguments[0]; const newPropertyType = arguments[1]; const column1Parts = arguments[2]; const operator = arguments[3]; const column2Parts = arguments[4]; this.extraJoinedProperties.push({ name: newPropertyKey, propertyType: newPropertyType, }); const tableToJoinClass = newPropertyType; const tableToJoinName = getTableMetadata(tableToJoinClass).tableName; const tableToJoinAlias = newPropertyKey; const table1Column = this.getColumnName(...column1Parts); const table2Column = this.getColumnName(...column2Parts); this.queryBuilder.innerJoin(`${tableToJoinName} as ${tableToJoinAlias}`, table1Column, operator, table2Column); return this; } public innerJoinTableOnFunction() { return this.joinTableOnFunction(this.queryBuilder.innerJoin.bind(this.queryBuilder), arguments[0], arguments[1], arguments[2]); } public leftOuterJoinTableOnFunction() { return this.joinTableOnFunction(this.queryBuilder.leftOuterJoin.bind(this.queryBuilder), arguments[0], arguments[1], arguments[2]); } public leftOuterJoinTable() { const newPropertyKey = arguments[0]; const newPropertyType = arguments[1]; const column1Parts = arguments[2]; const operator = arguments[3]; const column2Parts = arguments[4]; this.extraJoinedProperties.push({ name: newPropertyKey, propertyType: newPropertyType, }); const tableToJoinClass = newPropertyType; const tableToJoinName = getTableMetadata(tableToJoinClass).tableName; const tableToJoinAlias = newPropertyKey; const table1Column = this.getColumnName(...column1Parts); const table2Column = this.getColumnName(...column2Parts); this.queryBuilder.leftOuterJoin(`${tableToJoinName} as ${tableToJoinAlias}`, table1Column, operator, table2Column); return this; } public whereColumn() { // This is called from the sub-query // The first column is from the sub-query // The second column is from the parent query let column1Name; let column2Name; if (typeof arguments[0] === 'string') { column1Name = this.getColumnName(...arguments[0].split('.')); if (!this.parentTypedQueryBuilder) { throw new Error('Parent query builder is missing, "whereColumn" can only be used in sub-query.'); } column2Name = this.parentTypedQueryBuilder.getColumnName(...arguments[2].split('.')); } else { column1Name = this.getColumnName(...this.getArgumentsFromColumnFunction(arguments[0])); if (typeof arguments[2] === 'string') { column2Name = arguments[2]; } else if (arguments[2].memories !== undefined) { column2Name = arguments[2].getColumnName; // parent this needed ... } else { column2Name = this.getColumnName(...this.getArgumentsFromColumnFunction(arguments[2])); } } const operator = arguments[1]; this.queryBuilder.whereRaw(`?? ${operator} ??`, [column1Name, column2Name]); return this; } public toQuery() { return this.queryBuilder.toQuery(); } public whereNull() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.whereNull.bind(this.queryBuilder), ...arguments); } public whereNotNull() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.whereNotNull.bind(this.queryBuilder), ...arguments); } public orWhereNull() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhereNull.bind(this.queryBuilder), ...arguments); } public orWhereNotNull() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhereNotNull.bind(this.queryBuilder), ...arguments); } public getArgumentsFromColumnFunction(f: any) { if (typeof f === 'string') { return f.split('.'); } const { root, memories } = getProxyAndMemories(); f(root); return memories; } public async findByPrimaryKey() { const primaryKeyColumnInfo = getPrimaryKeyColumn(this.tableClass); const primaryKeyValue = arguments[0]; let columnArgumentsList; if (typeof arguments[1] === 'string') { const [, ...columnArguments] = arguments; columnArgumentsList = columnArguments.map((concatKey: string) => concatKey.split('.')); } else { const f = arguments[1]; columnArgumentsList = this.getArgumentsFromColumnFunction3(f); } for (const columnArguments of columnArgumentsList) { this.queryBuilder.select(this.getColumnName(...columnArguments) + ' as ' + this.getColumnSelectAlias(...columnArguments)); } this.queryBuilder.where(primaryKeyColumnInfo.name, primaryKeyValue); if (this.onlyLogQuery) { this.queryLog += this.queryBuilder.toQuery() + '\n'; } else { return this.queryBuilder.first(); } } public where() { if (typeof arguments[0] === 'string') { return this.callKnexFunctionWithConcatKeyColumn(this.queryBuilder.where.bind(this.queryBuilder), ...arguments); } return this.callKnexFunctionWithColumnFunction(this.queryBuilder.where.bind(this.queryBuilder), ...arguments); } public whereNot() { if (typeof arguments[0] === 'string') { return this.callKnexFunctionWithConcatKeyColumn(this.queryBuilder.whereNot.bind(this.queryBuilder), ...arguments); } const columnArguments = this.getArgumentsFromColumnFunction(arguments[0]); this.queryBuilder.whereNot(this.getColumnName(...columnArguments), arguments[1]); return this; } public andWhere() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.andWhere.bind(this.queryBuilder), ...arguments); } public orWhere() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhere.bind(this.queryBuilder), ...arguments); } public whereIn() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.whereIn.bind(this.queryBuilder), ...arguments); } public whereNotIn() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.whereNotIn.bind(this.queryBuilder), ...arguments); } public orWhereIn() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhereIn.bind(this.queryBuilder), ...arguments); } public orWhereNotIn() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhereNotIn.bind(this.queryBuilder), ...arguments); } public whereBetween() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.whereBetween.bind(this.queryBuilder), ...arguments); } public whereNotBetween() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.whereNotBetween.bind(this.queryBuilder), ...arguments); } public orWhereBetween() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhereBetween.bind(this.queryBuilder), ...arguments); } public orWhereNotBetween() { return this.callKnexFunctionWithColumnFunction(this.queryBuilder.orWhereNotBetween.bind(this.queryBuilder), ...arguments); } public callQueryCallbackFunction(functionName: string, typeOfSubQuery: any, functionToCall: any) { const that = this; ((this.queryBuilder as any)[functionName] as (callback: Knex.QueryCallback) => Knex.QueryBuilder)(function () { const subQuery = this; const { root, memories } = getProxyAndMemories(that); const subQB = new TypedQueryBuilder(typeOfSubQuery, that.knex, subQuery, that); subQB.extraJoinedProperties = that.extraJoinedProperties; functionToCall(subQB, root, memories); }); } public selectQuery() { this.hasSelectClause = true; const name = arguments[0]; const typeOfSubQuery = arguments[2]; const functionToCall = arguments[3]; const { root, memories } = getProxyAndMemories(this); const subQueryBuilder = new TypedQueryBuilder(typeOfSubQuery, this.knex, undefined, this); functionToCall(subQueryBuilder, root, memories); (this.selectRaw as any)(name, undefined, subQueryBuilder.toQuery()); return this as any; } public whereParentheses() { this.callQueryCallbackFunction('where', this.tableClass, arguments[0]); return this; } public whereExists() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('whereExists', typeOfSubQuery, functionToCall); return this; } public orWhereExists() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('orWhereExists', typeOfSubQuery, functionToCall); return this; } public whereNotExists() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('whereNotExists', typeOfSubQuery, functionToCall); return this; } public orWhereNotExists() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('orWhereNotExists', typeOfSubQuery, functionToCall); return this; } public whereRaw(sql: string, ...bindings: string[]) { this.queryBuilder.whereRaw(sql, bindings); return this; } public having() { const operator = arguments[1]; const value = arguments[2]; this.queryBuilder.having(this.getColumnNameFromFunctionOrString(arguments[0]), operator, value); return this; } public havingIn() { const value = arguments[1]; this.queryBuilder.havingIn(this.getColumnNameFromFunctionOrString(arguments[0]), value); return this; } public havingNotIn() { const value = arguments[1]; (this.queryBuilder as any).havingNotIn(this.getColumnNameFromFunctionOrString(arguments[0]), value); return this; } public havingNull() { (this.queryBuilder as any).havingNull(this.getColumnNameFromFunctionOrString(arguments[0])); return this; } public havingNotNull() { (this.queryBuilder as any).havingNotNull(this.getColumnNameFromFunctionOrString(arguments[0])); return this; } public havingExists() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('havingExists', typeOfSubQuery, functionToCall); return this; } public havingNotExists() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('havingNotExists', typeOfSubQuery, functionToCall); return this; } public havingRaw(sql: string, ...bindings: string[]) { this.queryBuilder.havingRaw(sql, bindings); return this; } public havingBetween() { const value = arguments[1]; (this.queryBuilder as any).havingBetween(this.getColumnNameFromFunctionOrString(arguments[0]), value); return this; } public havingNotBetween() { const value = arguments[1]; (this.queryBuilder as any).havingNotBetween(this.getColumnNameFromFunctionOrString(arguments[0]), value); return this; } public orderByRaw(sql: string, ...bindings: string[]) { this.queryBuilder.orderByRaw(sql, bindings); return this; } public union() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('union', typeOfSubQuery, functionToCall); return this; } public unionAll() { const typeOfSubQuery = arguments[0]; const functionToCall = arguments[1]; this.callQueryCallbackFunction('unionAll', typeOfSubQuery, functionToCall); return this; } public returningColumn() { throw new NotImplementedError(); } public returningColumns() { throw new NotImplementedError(); } public transacting(trx: Knex.Transaction) { this.queryBuilder.transacting(trx); this.transaction = trx; return this; } public min() { return this.functionWithAlias('min', arguments[0], arguments[1]); } public count() { return this.functionWithAlias('count', arguments[0], arguments[1]); } public countDistinct() { return this.functionWithAlias('countDistinct', arguments[0], arguments[1]); } public max() { return this.functionWithAlias('max', arguments[0], arguments[1]); } public sum() { return this.functionWithAlias('sum', arguments[0], arguments[1]); } public sumDistinct() { return this.functionWithAlias('sumDistinct', arguments[0], arguments[1]); } public avg() { return this.functionWithAlias('avg', arguments[0], arguments[1]); } public avgDistinct() { return this.functionWithAlias('avgDistinct', arguments[0], arguments[1]); } public increment() { const value = arguments[arguments.length - 1]; this.queryBuilder.increment(this.getColumnNameFromArgumentsIgnoringLastParameter(...arguments), value); return this; } public decrement() { const value = arguments[arguments.length - 1]; this.queryBuilder.decrement(this.getColumnNameFromArgumentsIgnoringLastParameter(...arguments), value); return this; } public async truncate() { await this.queryBuilder.truncate(); } public async insertSelect() { const tableName = getTableMetadata(arguments[0]).tableName; const typedQueryBuilderForInsert = new TypedQueryBuilder(arguments[0], this.knex); let columnArgumentsList; if (typeof arguments[1] === 'string') { const [, ...columnArguments] = arguments; columnArgumentsList = columnArguments.map((concatKey: string) => concatKey.split('.')); } else { const f = arguments[1]; columnArgumentsList = this.getArgumentsFromColumnFunction3(f); } const insertColumns = columnArgumentsList.map((i) => typedQueryBuilderForInsert.getColumnName(...i)); // https://github.com/knex/knex/issues/1056 const qb = this.knex.from(this.knex.raw(`?? (${insertColumns.map(() => '??').join(',')})`, [tableName, ...insertColumns])).insert(this.knex.raw(this.toQuery())); const finalQuery = qb.toString(); this.toQuery = () => finalQuery; await qb; } public clearSelect() { this.queryBuilder.clearSelect(); return this as any; } public clearWhere() { this.queryBuilder.clearWhere(); return this as any; } public clearOrder() { (this.queryBuilder as any).clearOrder(); return this as any; } public distinct() { this.queryBuilder.distinct(); return this as any; } public clone() { const queryBuilderClone = this.queryBuilder.clone(); const typedQueryBuilderClone = new TypedQueryBuilder(this.tableClass, this.knex, queryBuilderClone); return typedQueryBuilderClone as any; } public groupBy() { this.queryBuilder.groupBy(this.getColumnNameFromFunctionOrString(arguments[0])); return this; } public groupByRaw(sql: string, ...bindings: string[]) { this.queryBuilder.groupByRaw(sql, bindings); return this; } public useKnexQueryBuilder(f: (query: Knex.QueryBuilder) => void) { f(this.queryBuilder); return this; } public getColumnName(...keys: string[]): string { const firstPartName = this.getColumnNameWithoutAlias(keys[0]); if (keys.length === 1) { return firstPartName; } else { let columnName; let columnAlias; let currentClass; let currentColumnPart; const extraJoinedProperty = this.extraJoinedProperties.find((i) => i.name === keys[0]); if (extraJoinedProperty) { columnName = ''; columnAlias = extraJoinedProperty.name; currentClass = extraJoinedProperty.propertyType; } else { currentColumnPart = getColumnInformation(this.tableClass, keys[0]); columnName = ''; columnAlias = currentColumnPart.propertyKey; currentClass = currentColumnPart.columnClass; } for (let i = 1; i < keys.length; i++) { currentColumnPart = getColumnInformation(currentClass, keys[i]); columnName = columnAlias + '.' + (keys.length - 1 === i ? currentColumnPart.name : currentColumnPart.propertyKey); columnAlias += '_' + (keys.length - 1 === i ? currentColumnPart.name : currentColumnPart.propertyKey); currentClass = currentColumnPart.columnClass; } return columnName; } } public getColumnNameWithDifferentRoot(_rootKey: string, ...keys: string[]): string { const firstPartName = this.getColumnNameWithoutAlias(keys[0]); if (keys.length === 1) { return firstPartName; } else { let currentColumnPart = getColumnInformation(this.tableClass, keys[0]); let columnName = ''; let columnAlias = currentColumnPart.propertyKey; let currentClass = currentColumnPart.columnClass; for (let i = 0; i < keys.length; i++) { currentColumnPart = getColumnInformation(currentClass, keys[i]); columnName = columnAlias + '.' + (keys.length - 1 === i ? currentColumnPart.name : currentColumnPart.propertyKey); columnAlias += '_' + (keys.length - 1 === i ? currentColumnPart.name : currentColumnPart.propertyKey); currentClass = currentColumnPart.columnClass; } return columnName; } } private functionWithAlias(knexFunctionName: string, f: any, aliasName: string) { this.hasSelectClause = true; (this.queryBuilder as any)[knexFunctionName](`${this.getColumnNameWithoutAliasFromFunctionOrString(f)} as ${aliasName}`); return this as any; } private getColumnNameFromFunctionOrString(f: any) { let columnParts; if (typeof f === 'string') { columnParts = f.split('.'); } else { columnParts = this.getArgumentsFromColumnFunction(f); } return this.getColumnName(...columnParts); } private getColumnNameWithoutAliasFromFunctionOrString(f: any) { let columnParts; if (typeof f === 'string') { columnParts = f.split('.'); } else { columnParts = this.getArgumentsFromColumnFunction(f); } return this.getColumnNameWithoutAlias(...columnParts); } private joinColumn(joinType: 'innerJoin' | 'leftOuterJoin', f: any) { let columnToJoinArguments: string[]; if (typeof f === 'string') { columnToJoinArguments = f.split('.'); } else { columnToJoinArguments = this.getArgumentsFromColumnFunction(f); } const columnToJoinName = this.getColumnName(...columnToJoinArguments); let secondColumnName = columnToJoinArguments[0]; let secondColumnAlias = columnToJoinArguments[0]; let secondColumnClass = getColumnInformation(this.tableClass, secondColumnName).columnClass; for (let i = 1; i < columnToJoinArguments.length; i++) { const beforeSecondColumnAlias = secondColumnAlias; const beforeSecondColumnClass = secondColumnClass; const columnInfo = getColumnInformation(beforeSecondColumnClass, columnToJoinArguments[i]); secondColumnName = columnInfo.name; secondColumnAlias = beforeSecondColumnAlias + '_' + columnInfo.propertyKey; secondColumnClass = columnInfo.columnClass; } const tableToJoinName = getTableMetadata(secondColumnClass).tableName; const tableToJoinAlias = secondColumnAlias; const tableToJoinJoinColumnName = `${tableToJoinAlias}.${getPrimaryKeyColumn(secondColumnClass).name}`; if (joinType === 'innerJoin') { this.queryBuilder.innerJoin(`${tableToJoinName} as ${tableToJoinAlias}`, tableToJoinJoinColumnName, columnToJoinName); } else if (joinType === 'leftOuterJoin') { this.queryBuilder.leftOuterJoin(`${tableToJoinName} as ${tableToJoinAlias}`, tableToJoinJoinColumnName, columnToJoinName); } return this; } private getColumnNameFromArgumentsIgnoringLastParameter(...keys: string[]): string { const argumentsExceptLast = keys.slice(0, -1); return this.getColumnName(...argumentsExceptLast); } private getColumnNameWithoutAlias(...keys: string[]): string { const extraJoinedProperty = this.extraJoinedProperties.find((i) => i.name === keys[0]); if (extraJoinedProperty) { if (keys.length === 1) { return extraJoinedProperty.name; } const columnInfo = getColumnInformation(extraJoinedProperty.propertyType, keys[1]); return extraJoinedProperty.name + '.' + columnInfo.name; } if (keys.length === 1) { // const extraJoinedProperty = this.extraJoinedProperties.find( // i => i.name === keys[0] // ); // if (extraJoinedProperty) { // return extraJoinedProperty.name; // // if (keys.length === 1) { // // } // // const columnInfo = getColumnInformation( // // extraJoinedProperty.propertyType, // // keys[1] // // ); // // return extraJoinedProperty.name + '.' + columnInfo.name; // } const columnInfo = getColumnInformation(this.tableClass, keys[0]); return this.tableName + '.' + columnInfo.name; } else { let currentColumnPart = getColumnInformation(this.tableClass, keys[0]); let result = currentColumnPart.propertyKey; let currentClass = currentColumnPart.columnClass; for (let i = 1; i < keys.length; i++) { currentColumnPart = getColumnInformation(currentClass, keys[i]); result += '.' + (keys.length - 1 === i ? currentColumnPart.name : currentColumnPart.propertyKey); currentClass = currentColumnPart.columnClass; } return result; } } private getColumnSelectAlias(...keys: string[]): string { if (keys.length === 1) { return keys[0]; } else { let columnAlias = keys[0]; for (let i = 1; i < keys.length; i++) { columnAlias += '.' + keys[i]; } return columnAlias; } } private flattenByOption(o: any, flattenOption?: FlattenOption) { if (flattenOption === FlattenOption.noFlatten || this.shouldUnflatten === false) { return o; } const unflattened = unflatten(o); if (flattenOption === undefined || flattenOption === FlattenOption.flatten) { return unflattened; } return setToNull(unflattened); } private joinTableOnFunction(queryBuilderJoin: Knex.Join, newPropertyKey: any, newPropertyType: any, onFunction: (join: IJoinOnClause2) => void) { this.extraJoinedProperties.push({ name: newPropertyKey, propertyType: newPropertyType, }); const tableToJoinClass = newPropertyType; const tableToJoinName = getTableMetadata(tableToJoinClass).tableName; const tableToJoinAlias = newPropertyKey; let knexOnObject: any; queryBuilderJoin(`${tableToJoinName} as ${tableToJoinAlias}`, function () { knexOnObject = this; }); const onWithJoinedColumnOperatorColumn = (joinedColumn: any, operator: any, modelColumn: any, functionName: string) => { let column1Arguments; let column2Arguments; if (typeof modelColumn === 'string') { column1Arguments = modelColumn.split('.'); column2Arguments = joinedColumn.split('.'); } else { column1Arguments = this.getArgumentsFromColumnFunction(modelColumn); column2Arguments = this.getArgumentsFromColumnFunction(joinedColumn); } const column2ArgumentsWithJoinedTable = [tableToJoinAlias, ...column2Arguments]; knexOnObject[functionName](this.getColumnName(...column1Arguments), operator, column2ArgumentsWithJoinedTable.join('.')); }; const onWithColumnOperatorValue = (joinedColumn: any, operator: any, value: any, functionName: string) => { // const column1Arguments = this.getArgumentsFromColumnFunction( // joinedColumn // ); const column2Arguments = this.getArgumentsFromColumnFunction(joinedColumn); const column2ArgumentsWithJoinedTable = [tableToJoinAlias, ...column2Arguments]; knexOnObject[functionName]( // this.getColumnName(...column1Arguments), column2ArgumentsWithJoinedTable.join('.'), operator, value // column2ArgumentsWithJoinedTable.join('.') ); }; const onObject = { onColumns: (column1: any, operator: any, column2: any) => { onWithJoinedColumnOperatorColumn(column2, operator, column1, 'on'); return onObject; }, on: (column1: any, operator: any, column2: any) => { onWithJoinedColumnOperatorColumn(column1, operator, column2, 'on'); return onObject; }, andOn: (column1: any, operator: any, column2: any) => { onWithJoinedColumnOperatorColumn(column1, operator, column2, 'andOn'); return onObject; }, orOn: (column1: any, operator: any, column2: any) => { onWithJoinedColumnOperatorColumn(column1, operator, column2, 'orOn'); return onObject; }, onVal: (column1: any, operator: any, value: any) => { onWithColumnOperatorValue(column1, operator, value, 'onVal'); return onObject; }, andOnVal: (column1: any, operator: any, value: any) => { onWithColumnOperatorValue(column1, operator, value, 'andOnVal'); return onObject; }, orOnVal: (column1: any, operator: any, value: any) => { onWithColumnOperatorValue(column1, operator, value, 'orOnVal'); return onObject; }, onNull: (f: any) => { const column2Arguments = this.getArgumentsFromColumnFunction(f); const column2ArgumentsWithJoinedTable = [tableToJoinAlias, ...column2Arguments]; knexOnObject.onNull(column2ArgumentsWithJoinedTable.join('.')); return onObject; }, } as any; onFunction(onObject as any); return this as any; } private callKnexFunctionWithColumnFunction(knexFunction: any, ...args: any[]) { if (typeof args[0] === 'string') { return this.callKnexFunctionWithConcatKeyColumn(knexFunction, ...args); } const columnArguments = this.getArgumentsFromColumnFunction(args[0]); if (args.length === 3) { knexFunction(this.getColumnName(...columnArguments), args[1], args[2]); } else { knexFunction(this.getColumnName(...columnArguments), args[1]); } return this; } private callKnexFunctionWithConcatKeyColumn(knexFunction: any, ...args: any[]) { const columnName = this.getColumnName(...args[0].split('.')); if (args.length === 3) { knexFunction(columnName, args[1], args[2]); } else { knexFunction(columnName, args[1]); } return this; } private selectAllModelProperties() { const properties = getColumnProperties(this.tableClass); for (const property of properties) { this.queryBuilder.select(`${property.name} as ${property.propertyKey}`); } } }