import * as knex from 'knex' ; import { Query } from './Query' ; import { JoinColumn , Mapping , Relationship } from './Mapping' ; import { Entity , Scope } from './Scope' ; import { Catalogue , Hydrator } from './Hydrator' ; import { Having } from './Criteria/Having' ; import { Where } from './Criteria/Where' ; import { On } from './Criteria/On' ; export class QueryBuilder < T > { type { { } } public mappings : { [ key ] : Mapping < Entity > ; } ; type { Query } private query : Query ; type { boolean } private prepared : boolean = false ; type { string } private alias : string ; type { Scope } private entityManager : Scope ; type { { } } private statement : knex . QueryBuilder ; type { Array } private selects : Array < any > = [ ] ; type { { } private appliedPrimaryKeys : { [ key ] : string ; } = { } ; type { Array } private groupBys : Array < { groupBy : string | Array < string > ; } > = [ ] ; type { QueryBuilder } private derivedFrom : { derived : QueryBuilder < T > ; alias : string ; } ; type { Array } private orderBys : Array < { orderBy : string | Array < string > | Object ; direction : string | null ; } > = [ ] ; type { Where } private whereCriteria : Where ; type { On } private onCriteria ; type { Having } private havingCriteria : Having ; type { string [ ] } private functions : Array < string > = [ 'sum' , 'count' , 'max' , 'min' , 'avg' , 'distinct' ] ; type { string [ ] } private singleJoinTypes : Array < string > = [ Mapping . RELATION_ONE_TO_ONE , Mapping . RELATION_MANY_TO_ONE ] ; type { Hydrator } private readonly hydrator : Hydrator ; type { boolean } private readonly managed : boolean = true ; type { { } } private aliased : { } = { } ; private children : Array < QueryBuilder < { new ( ) ; } > > = [ ] ; private queryBuilders : { [ key ] : QueryBuilder < { new ( ) ; } > ; } = { } ; param { Scope } entityManager param { knex . QueryBuilder } statement param { Mapping } mapping param { string } alias param { boolean } managed public constructor ( entityManager , statement , mapping , alias , managed = true ) { this . alias = alias ; this . mappings = { [ alias ] : mapping } ; this . statement = statement ; this . managed = managed ; this . whereCriteria = new Where ( this . statement , mapping , this . mappings , alias ) ; this . havingCriteria = new Having ( this . statement , mapping , this . mappings , alias ) ; this . onCriteria = new On ( this . statement , mapping , this . mappings , alias ) ; this . entityManager = entityManager ; this . hydrator = new Hydrator ( entityManager , managed ) ; this . query = new Query ( statement , this . hydrator , this . children ) ; this . hydrator . addRecipe ( null , alias , this . mappings [ alias ] ) ; } param { string } target returns { string } public createAlias ( target ) { this . aliased [ target ] = this . aliased [ target ] || 0 ; return target + this . aliased [ target ] ++ ; } param { string } joinMethod param { string } column param { string } targetAlias returns { QueryBuilder } public makeJoin ( joinMethod , column , targetAlias ) { const { owningMapping , join , property , alias } = this . getRelationship ( column ) ; const TargetReference = this . entityManager . resolveEntityReference ( join . targetEntity ) ; this . mappings [ targetAlias ] = this . mappings [ targetAlias ] || Mapping . forEntity ( TargetReference ) ; const targetMapping = this . mappings [ targetAlias ] ; const joinType = this . singleJoinTypes . indexOf ( join . type ) > - 1 ? 'single' : 'collection' ; let joinColumn = owningMapping . getJoinColumn ( property ) ; let owning = alias ; let inversed = targetAlias ; this . hydrator . addRecipe ( alias , targetAlias , targetMapping , joinType , property ) ; if ( join . type === Mapping . RELATION_MANY_TO_MANY ) { let joinTable ; let joinColumns ; let inverseJoinColumns ; if ( join . inversedBy ) { joinTable = owningMapping . getJoinTable ( property ) ; joinColumns = joinTable . joinColumns ; inverseJoinColumns = joinTable . inverseJoinColumns ; } else { joinTable = targetMapping . getJoinTable ( join . mappedBy ) ; joinColumns = joinTable . inverseJoinColumns ; inverseJoinColumns = joinTable . joinColumns ; } const joinTableAlias = this . createAlias ( joinTable . name ) ; const onCriteriaOwning = { } ; joinColumns . forEach ( ( joinColumn ) => { onCriteriaOwning [ `${ owning }.${ joinColumn . referencedColumnName }` ] = `${ joinTableAlias }.${ joinColumn . name }` ; } ) ; this . join ( joinMethod , joinTable . name , joinTableAlias , onCriteriaOwning ) ; const onCriteriaInversed = { } ; inverseJoinColumns . forEach ( ( inverseJoinColumn ) => { onCriteriaInversed [ `${ joinTableAlias }.${ inverseJoinColumn . name }` ] = `${ inversed }.${ inverseJoinColumn . referencedColumnName }` ; } ) ; this . join ( joinMethod , targetMapping . getTableName ( ) , inversed , onCriteriaInversed ) ; return this ; } if ( join . mappedBy ) { joinColumn = targetMapping . getJoinColumn ( join . mappedBy ) ; owning = inversed ; inversed = alias ; } const onCriteria = { [ `${ owning }.${ joinColumn . name }` ] : `${ inversed }.${ joinColumn . referencedColumnName }` } ; this . join ( joinMethod , targetMapping . getTableName ( ) , targetAlias , onCriteria ) ; return this ; } param { string } joinMethod param { string } table param { string } alias param { { } } on returns { QueryBuilder } public join ( joinMethod , table , alias , on ) { this . statement [ joinMethod ] ( `${ table } as ${ alias }` , statement => { this . onCriteria . stage ( on , undefined , statement ) ; } ) ; return this ; } param { string } column param { string } targetAlias returns { QueryBuilder } public leftJoin ( column , targetAlias ) { return this . makeJoin ( 'leftJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public innerJoin ( column , targetAlias ) { return this . makeJoin ( 'innerJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public leftOuterJoin ( column , targetAlias ) { return this . makeJoin ( 'leftOuterJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public rightJoin ( column , targetAlias ) { return this . makeJoin ( 'rightJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public rightOuterJoin ( column , targetAlias ) { return this . makeJoin ( 'rightOuterJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public outerJoin ( column , targetAlias ) { return this . makeJoin ( 'outerJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public fullOuterJoin ( column , targetAlias ) { return this . makeJoin ( 'fullOuterJoin' , column , targetAlias ) ; } param { string } column param { string } targetAlias returns { QueryBuilder } public crossJoin ( column , targetAlias ) { return this . makeJoin ( 'crossJoin' , column , targetAlias ) ; } param { string } alias returns { QueryBuilder } public getChild ( alias ) { return this . queryBuilders [ alias ] ; } param { QueryBuilder } child returns { QueryBuilder } public addChild ( child ) { this . children . push ( child ) ; return this ; } param { string } column param { string } targetAlias returns { QueryBuilder } public quickJoin ( column , targetAlias ? ) { const { join , alias , property } = this . getRelationship ( column ) ; const parentQueryBuilder = this . getChild ( alias ) || this ; targetAlias = targetAlias || parentQueryBuilder . createAlias ( property ) ; if ( join . type !== Mapping . RELATION_MANY_TO_MANY && join . type !== Mapping . RELATION_ONE_TO_MANY ) { return parentQueryBuilder . leftJoin ( column , targetAlias ) ; } const childQueryBuilder = parentQueryBuilder . populate ( column , null , targetAlias ) ; this . queryBuilders [ targetAlias ] = childQueryBuilder ; return childQueryBuilder ; } param { string } column param { QueryBuilder } queryBuilder param { string } targetAlias returns { QueryBuilder < { new ( ) } > } public populate ( column , queryBuilder ? , targetAlias ? ) { const { owningMapping , join , property , alias } = this . getRelationship ( column ) ; if ( join . type !== Mapping . RELATION_MANY_TO_MANY && join . type !== Mapping . RELATION_ONE_TO_MANY ) { throw new Error ( `It's not possible to populate relations with type '${ join . type }', target must be a collection.` ) ; } const parentQueryBuilder = this . getChild ( alias ) || this ; const TargetReference = this . entityManager . resolveEntityReference ( join . targetEntity ) ; targetAlias = targetAlias || parentQueryBuilder . createAlias ( property ) ; parentQueryBuilder . mappings [ targetAlias ] = parentQueryBuilder . mappings [ targetAlias ] || Mapping . forEntity ( TargetReference ) ; if ( ! ( queryBuilder instanceof QueryBuilder ) ) { queryBuilder = this . entityManager . getRepository ( TargetReference ) . getQueryBuilder ( targetAlias ) ; } const targetMapping = queryBuilder . getHostMapping ( ) ; this . queryBuilders [ targetAlias ] = queryBuilder ; let parentColumn ; parentQueryBuilder . addChild ( queryBuilder ) ; if ( join . type === Mapping . RELATION_ONE_TO_MANY ) { parentColumn = `${ targetAlias }.${ targetMapping . getJoinColumn ( join . mappedBy ) . name }` ; } else { let joinTable ; let joinColumn ; let joinTableAlias ; if ( join . inversedBy ) { joinTable = owningMapping . getJoinTable ( property ) ; joinColumn = joinTable . inverseJoinColumns [ 0 ] ; parentColumn = joinTable . joinColumns [ 0 ] . name ; } else { joinTable = targetMapping . getJoinTable ( join . mappedBy ) ; joinColumn = joinTable . joinColumns [ 0 ] ; parentColumn = joinTable . inverseJoinColumns [ 0 ] . name ; } joinTableAlias = queryBuilder . createAlias ( joinTable . name ) ; parentColumn = `${ joinTableAlias }.${ parentColumn }` ; queryBuilder . join ( 'innerJoin' , joinTable . name , joinTableAlias , { [ `${ targetAlias }.${ joinColumn . referencedColumnName }` ] : `${ joinTableAlias }.${ joinColumn . name }` , } ) ; } const hydrator = parentQueryBuilder . getHydrator ( ) ; hydrator . getRecipe ( ) . hydrate = true ; if ( ! hydrator . hasCatalogue ( alias ) ) { this . applyPrimaryKeySelect ( alias ) ; } return queryBuilder . setParent ( property , parentColumn , hydrator . enableCatalogue ( alias ) ) ; } param { string } property param { string } column param { Catalogue } catalogue returns { QueryBuilder } public setParent ( property , column , catalogue ) { this . statement . select ( `${ column } as ${ column }` ) ; this . query . setParent ( { column , primaries : catalogue . primaries } ) ; this . hydrator . getRecipe ( ) . parent = { entities : catalogue . entities , column , property } ; return this ; } returns { Query } public getQuery ( ) { this . prepare ( ) ; return this . query ; } param { string [ ] | string | { } } alias returns { QueryBuilder } public select ( alias ) { this . selects . push ( ... arguments ) ; this . prepared = false ; return this ; } returns { string } public getAlias ( ) { return this . alias ; } returns { knex . QueryBuilder } public getStatement ( ) { return this . statement ; } returns { Mapping < Entity > } public getHostMapping ( ) { return this . mappings [ this . alias ] ; } returns { Hydrator } public getHydrator ( ) { return this . hydrator ; } returns { QueryBuilder } public prepare ( ) { if ( this . prepared ) { return this ; } this . whereCriteria . applyStaged ( ) ; this . havingCriteria . applyStaged ( ) ; this . onCriteria . applyStaged ( ) ; this . applyFrom ( ) ; this . applySelects ( ) ; this . applyOrderBys ( ) ; this . applyGroupBys ( ) ; this . prepared = true ; return this ; } param { { } } values param { string } returning returns { QueryBuilder } public insert ( values , returning ? ) { this . setCriteriaHostAlias ( ) ; if ( this . statement [ 'client' ] . config . client === 'sqlite3' || this . statement [ 'client' ] . config . client === 'sqlite' ) { this . statement . insert ( this . mapToColumns ( values ) ) ; } else { this . statement . insert ( this . mapToColumns ( values ) , returning ) ; } return this ; } param { { } } values param { string } returning returns { QueryBuilder } public update ( values , returning ? ) { this . setCriteriaHostAlias ( ) ; this . statement . from ( this . mappings [ this . alias ] . getTableName ( ) ) ; if ( this . statement [ 'client' ] . config . client === 'sqlite3' || this . statement [ 'client' ] . config . client === 'sqlite' ) { this . statement . update ( this . mapToColumns ( values ) ) ; } else { this . statement . update ( this . mapToColumns ( values ) , returning ) ; } return this ; } param { number } limit returns { QueryBuilder } public limit ( limit ) { this . statement . limit ( limit ) ; return this ; } param { number } offset returns { QueryBuilder } public offset ( offset ) { this . statement . offset ( offset ) ; return this ; } param { string | string [ ] } groupBy returns { QueryBuilder } public groupBy ( groupBy ) { this . groupBys . push ( { groupBy } ) ; return this ; } param { string | string [ ] | { } } orderBy param { string } direction returns { QueryBuilder } public orderBy ( orderBy , direction ? ) { this . orderBys . push ( { orderBy , direction } ) ; return this ; } returns { QueryBuilder } public remove ( ) { this . setCriteriaHostAlias ( ) ; this . statement . from ( this . mappings [ this . alias ] . getTableName ( ) ) ; this . statement . del ( ) ; return this ; } param { { } } criteria returns { QueryBuilder } public where ( criteria ) { if ( ! Object . getOwnPropertyNames ( criteria ) . length ) { return this ; } this . whereCriteria . stage ( criteria ) ; this . prepared = false ; return this ; } param { QueryBuilder } derived param { string } alias returns { QueryBuilder } public from ( derived , alias ? ) { if ( ! alias ) { alias = this . createAlias ( derived . getHostMapping ( ) . getTableName ( ) ) ; } this . derivedFrom = { alias , derived } ; this . prepared = false ; return this ; } param { { } } criteria returns { QueryBuilder } public having ( criteria ) { if ( ! Object . getOwnPropertyNames ( criteria ) . length ) { return this ; } this . havingCriteria . stage ( criteria ) ; this . prepared = false ; return this ; } param { string } column returns { { } } private getRelationship ( column ) { column = column . indexOf ( '.' ) > - 1 ? column : `${ this . alias }.${ column }` ; const [ alias , property ] = column . split ( '.' ) ; const parent = this . getChild ( alias ) || this ; const owningMapping = parent . mappings [ alias ] ; let field ; if ( ! owningMapping ) { throw new Error ( `Cannot find the reference mapping for '${ alias }', are you sure you registered it first?` ) ; } if ( property ) { field = owningMapping . getField ( property , true ) ; } if ( ! field || ! field . relationship ) { throw new Error ( `Invalid relation supplied for join. Property '${ property }' not found on entity, or relation not defined.
        Are you registering the joins in the wrong order?` ) ; } return { owningMapping , join : field . relationship , property , alias } ; } returns { QueryBuilder } private applyFrom ( ) { if ( this . derivedFrom ) { const { derived , alias } = this . derivedFrom ; this . statement . from ( this . statement [ 'client' ] . raw ( `(${ derived . getQuery ( ) . getSQL ( ) }) as ${ alias }` ) ) ; this . derivedFrom = null ; } return this ; } returns { QueryBuilder } private applySelects ( ) { this . selects . forEach ( select => this . applySelect ( select ) ) ; this . selects = [ ] ; return this ; } param { [ ] } propertyAlias returns { QueryBuilder } private applySelect ( propertyAlias ) { if ( Array . isArray ( propertyAlias ) ) { propertyAlias . forEach ( value => this . applySelect ( value ) ) ; return this ; } if ( typeof propertyAlias === 'string' ) { return this . applyRegularSelect ( propertyAlias ) ; } if ( typeof propertyAlias !== 'object' ) { throw new Error ( `Unexpected value "${ propertyAlias }" of type "${ typeof propertyAlias }" for .select()` ) ; } const select = Object . getOwnPropertyNames ( propertyAlias ) ; const functionName = select [ 0 ] === 'alias' ? select [ 1 ] : select [ 0 ] ; const alias = propertyAlias [ select [ 0 ] === 'alias' ? select [ 0 ] : select [ 1 ] ] ; const fieldName = this . whereCriteria . mapToColumn ( propertyAlias [ functionName ] ) ; if ( this . functions . indexOf ( functionName ) === - 1 ) { throw new Error ( `Unknown function "${ select [ 0 ] }" specified.` ) ; } select . length > 1 ? this . statement [ functionName ] ( `${ fieldName } as ${ alias }` ) : this . statement [ functionName ] ( fieldName ) ; return this ; } param { string } propertyAlias returns { QueryBuilder } private applyRegularSelect ( propertyAlias ) { let alias = this . alias ; if ( propertyAlias . indexOf ( '.' ) === - 1 && ! this . mappings [ propertyAlias ] ) { propertyAlias = `${ alias }.${ propertyAlias }` ; } const selectAliases = [ ] ; const hydrateColumns = { } ; if ( propertyAlias . indexOf ( '.' ) > - 1 ) { const parts = propertyAlias . split ( '.' ) ; const property = parts [ 1 ] ; const column = this . whereCriteria . mapToColumn ( propertyAlias ) ; hydrateColumns [ column ] = property ; alias = parts [ 0 ] ; this . applyPrimaryKeySelect ( alias ) ; selectAliases . push ( `${ column } as ${ column }` ) ; } else { const mapping = this . mappings [ propertyAlias ] ; const fields = mapping . getFields ( ) ; alias = propertyAlias ; Object . getOwnPropertyNames ( fields ) . forEach ( field => { if ( ! fields [ field ] . relationship ) { const fieldName = fields [ field ] . name || ( fields [ field ] . primary ? 'id' : null ) ; if ( ! fieldName ) { throw new Error ( `Trying to query for field without a name for '${ mapping . getEntityName ( ) }.${ field }'.` ) ; } const fieldAlias = ( propertyAlias ? propertyAlias + '.' : '' ) + fieldName ; hydrateColumns [ fieldAlias ] = field ; selectAliases . push ( `${ fieldAlias } as ${ fieldAlias }` ) ; } } ) ; } this . statement . select ( selectAliases ) ; this . hydrator . getRecipe ( alias ) . hydrate = true ; this . hydrator . addColumns ( alias , hydrateColumns ) ; return this ; } param { string } alias returns { QueryBuilder } private applyPrimaryKeySelect ( alias ) { if ( ! this . managed ) { return this ; } if ( ! this . appliedPrimaryKeys [ alias ] ) { const aliasRecipe = this . hydrator . getRecipe ( alias ) ; this . appliedPrimaryKeys [ alias ] = `${ aliasRecipe . primaryKey . alias } as ${ aliasRecipe . primaryKey . alias }` ; this . statement . select ( this . appliedPrimaryKeys [ alias ] ) ; } return this ; } param { string | string [ ] } groupBy returns { QueryBuilder } private applyGroupBy ( groupBy ) { const properties = [ ] ; if ( typeof groupBy === 'string' ) { properties . push ( this . whereCriteria . mapToColumn ( groupBy ) ) ; } else if ( Array . isArray ( groupBy ) ) { groupBy . forEach ( group => properties . push ( this . whereCriteria . mapToColumn ( group ) ) ) ; } this . statement . groupBy ( properties ) ; return this ; } returns { QueryBuilder } private applyGroupBys ( ) { this . groupBys . forEach ( groupBy => this . applyGroupBy ( groupBy . groupBy ) ) ; this . groupBys = [ ] ; return this ; } param { string | string [ ] | { } } orderBy param { string } direction returns { QueryBuilder } private applyOrderBy ( orderBy , direction ? ) { if ( typeof orderBy === 'string' ) { this . statement . orderBy ( this . whereCriteria . mapToColumn ( orderBy ) , direction ) ; } else if ( Array . isArray ( orderBy ) ) { orderBy . forEach ( order => this . applyOrderBy ( order ) ) ; } else if ( typeof orderBy === 'object' ) { const property = Object . keys ( orderBy ) [ 0 ] ; this . applyOrderBy ( property , orderBy [ property ] ) ; } return this ; } returns { QueryBuilder } private applyOrderBys ( ) { this . orderBys . forEach ( orderBy => this . applyOrderBy ( orderBy . orderBy , orderBy . direction ) ) ; this . orderBys = [ ] ; return this ; } param { string } hostAlias private setCriteriaHostAlias ( hostAlias = null ) { this . whereCriteria . setHostAlias ( hostAlias ) ; this . onCriteria . setHostAlias ( hostAlias ) ; this . havingCriteria . setHostAlias ( hostAlias ) ; return this ; } param { { } [ ] } values returns { { } [ ] | { } } private mapToColumns ( values ) { let mappedValues ; if ( Array . isArray ( values ) ) { mappedValues = [ ] ; values . forEach ( value => { mappedValues . push ( this . mapToColumns ( value ) ) ; } ) ; return mappedValues ; } mappedValues = { } ; Object . getOwnPropertyNames ( values ) . forEach ( property => { let value = values [ property ] ; let fieldName , type ; if ( property . indexOf ( '.' ) > - 1 ) { const parts = property . split ( '.' ) ; if ( this . mappings [ parts [ 0 ] ] . isRelation ( parts [ 1 ] ) && typeof value === 'object' ) { return ; } parts [ 1 ] = this . mappings [ parts [ 0 ] ] . getFieldName ( parts [ 1 ] , parts [ 1 ] ) ; type = this . mappings [ parts [ 0 ] ] . getType ( parts [ 1 ] ) ; fieldName = parts . join ( '.' ) ; } else { if ( this . mappings [ this . alias ] . isRelation ( property ) && typeof value === 'object' ) { return ; } fieldName = this . mappings [ this . alias ] . getFieldName ( property , property ) ; type = this . mappings [ this . alias ] . getType ( property ) ; } if ( ! fieldName ) { throw new Error ( `No field name found in mapping for ${ this . mappings [ this . alias ] . getEntityName ( ) }::${ property }.` ) ; } if ( type === 'json' ) { value = JSON . stringify ( value ) ; } mappedValues [ fieldName ] = value ; } ) ; return mappedValues ; } }