Skip to content

Commit

Permalink
feat(QueryBuilder): add populate and quickJoin
Browse files Browse the repository at this point in the history
  • Loading branch information
RWOverdijk committed Jan 4, 2017
1 parent e702454 commit cdf6880
Showing 1 changed file with 249 additions and 44 deletions.
293 changes: 249 additions & 44 deletions src/QueryBuilder.ts
@@ -1,8 +1,8 @@
import * as knex from 'knex'; import * as knex from 'knex';
import {Query} from './Query'; import {Query} from './Query';
import {Mapping, JoinColumn} from './Mapping'; import {Mapping, JoinColumn, Relationship} from './Mapping';
import {Scope, Entity} from './Scope'; import {Scope, Entity} from './Scope';
import {Hydrator} from './Hydrator'; import {Hydrator, Catalogue} from './Hydrator';
import {Having} from './Criteria/Having'; import {Having} from './Criteria/Having';
import {Where} from './Criteria/Where'; import {Where} from './Criteria/Where';
import {On} from './Criteria/On'; import {On} from './Criteria/On';
Expand Down Expand Up @@ -53,11 +53,6 @@ export class QueryBuilder<T> {
*/ */
private orderBys: Array<{orderBy: string | Array<string> | Object, direction: string | null}> = []; private orderBys: Array<{orderBy: string | Array<string> | Object, direction: string | null}> = [];


/**
* @type {Mapping}
*/
private mapping: Mapping<T>;

/** /**
* @type {Where} * @type {Where}
*/ */
Expand All @@ -76,7 +71,7 @@ export class QueryBuilder<T> {
/** /**
* @type {{}} * @type {{}}
*/ */
private mappings: {[key: string]: Mapping<Entity>}; public mappings: {[key: string]: Mapping<Entity>};


/** /**
* @type {string[]} * @type {string[]}
Expand All @@ -98,6 +93,11 @@ export class QueryBuilder<T> {
*/ */
private aliased: {} = {}; private aliased: {} = {};


private children: Array<QueryBuilder<{new ()}>> = [];

private queryBuilders: {[key: string]: QueryBuilder<{new ()}>} = {};


/** /**
* Construct a new QueryBuilder. * Construct a new QueryBuilder.
* *
Expand All @@ -115,7 +115,7 @@ export class QueryBuilder<T> {
this.onCriteria = new On(this.statement, mapping, this.mappings); this.onCriteria = new On(this.statement, mapping, this.mappings);
this.entityManager = entityManager; this.entityManager = entityManager;
this.hydrator = new Hydrator(entityManager); this.hydrator = new Hydrator(entityManager);
this.query = new Query(statement, this.hydrator); this.query = new Query(statement, this.hydrator, this.children);


this.hydrator.addRecipe(null, alias, this.mappings[alias]); this.hydrator.addRecipe(null, alias, this.mappings[alias]);
} }
Expand Down Expand Up @@ -143,47 +143,38 @@ export class QueryBuilder<T> {
* @returns {QueryBuilder} * @returns {QueryBuilder}
*/ */
public makeJoin(joinMethod: string, column: string, targetAlias: string): this { public makeJoin(joinMethod: string, column: string, targetAlias: string): this {
column = column.indexOf('.') > -1 ? column : `${this.alias}.${column}`; let {owningMapping, join, property, alias} = this.getRelationship(column);
let [alias, property] = column.split('.'); let TargetReference = this.entityManager.resolveEntityReference(join.targetEntity);
let owningMapping = this.mappings[alias]; this.mappings[targetAlias] = this.mappings[targetAlias] || Mapping.forEntity(TargetReference);
let field; let targetMapping = this.mappings[targetAlias];

let joinType = this.singleJoinTypes.indexOf(join.type) > -1 ? 'single' : 'collection';
if (property) { let joinColumn = owningMapping.getJoinColumn(property);
field = owningMapping.getField(property, true); let owning = alias;
} let inversed = targetAlias;

if (!field || !field.relationship) {
throw new Error(
'Invalid relation supplied for join. Property not found on entity, or relation not defined. ' +
'Are you registering the joins in the wrong order?'
);
}

let join = field.relationship;
this.mappings[targetAlias] = Mapping.forEntity(this.entityManager.resolveEntityReference(join.targetEntity));
let targetMapping = this.mappings[targetAlias];
let 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); this.hydrator.addRecipe(alias, targetAlias, targetMapping, joinType, property);


if (join.type === Mapping.RELATION_MANY_TO_MANY) { if (join.type === Mapping.RELATION_MANY_TO_MANY) {
let joinTable; let joinTable;
let joinColumns;
let inverseJoinColumns;


if (join.inversedBy) { if (join.inversedBy) {
joinTable = owningMapping.getJoinTable(property); joinTable = owningMapping.getJoinTable(property);
joinColumns = joinTable.joinColumns;
inverseJoinColumns = joinTable.inverseJoinColumns;
} else { } else {
joinTable = targetMapping.getJoinTable(join.mappedBy); joinTable = targetMapping.getJoinTable(join.mappedBy);
joinColumns = joinTable.inverseJoinColumns;
inverseJoinColumns = joinTable.joinColumns;
} }


let joinTableAlias = this.createAlias(joinTable.name); let joinTableAlias = this.createAlias(joinTable.name);


// Join from owning to makeJoin-table. // Join from owning to makeJoin-table.
let onCriteriaOwning = {}; let onCriteriaOwning = {};


joinTable.joinColumns.forEach((joinColumn: JoinColumn) => { joinColumns.forEach((joinColumn: JoinColumn) => {
onCriteriaOwning[`${owning}.${joinColumn.referencedColumnName}`] = `${joinTableAlias}.${joinColumn.name}`; onCriteriaOwning[`${owning}.${joinColumn.referencedColumnName}`] = `${joinTableAlias}.${joinColumn.name}`;
}); });


Expand All @@ -192,7 +183,7 @@ export class QueryBuilder<T> {
// Join from makeJoin-table to inversed. // Join from makeJoin-table to inversed.
let onCriteriaInversed = {}; let onCriteriaInversed = {};


joinTable.inverseJoinColumns.forEach((inverseJoinColumn: JoinColumn) => { inverseJoinColumns.forEach((inverseJoinColumn: JoinColumn) => {
onCriteriaInversed[`${joinTableAlias}.${inverseJoinColumn.name}`] = `${inversed}.${inverseJoinColumn.referencedColumnName}`; onCriteriaInversed[`${joinTableAlias}.${inverseJoinColumn.name}`] = `${inversed}.${inverseJoinColumn.referencedColumnName}`;
}); });


Expand Down Expand Up @@ -330,6 +321,172 @@ export class QueryBuilder<T> {
return this.makeJoin('crossJoin', column, targetAlias); return this.makeJoin('crossJoin', column, targetAlias);
} }


/**
* Get a child querybuilder.
*
* @param {string} alias
*
* @returns {QueryBuilder}
*/
public getChild(alias: string): QueryBuilder<{new()}> {
return this.queryBuilders[alias];
}

/**
* Add a child to query.
*
* @param {QueryBuilder} child
*
* @returns {QueryBuilder}
*/
public addChild(child: QueryBuilder<{new ()}>): this {
this.children.push(child);

return this;
}

/**
* Figure out if given target is a collection. If so, populate. Otherwise, left join.
*/
public quickJoin(column: string, targetAlias?: string) {
let {join, alias, property} = this.getRelationship(column);
let 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);
}

// Collections need to be fetched individually.
let childQueryBuilder = parentQueryBuilder.populate(column, null, targetAlias);
this.queryBuilders[targetAlias] = childQueryBuilder;

return childQueryBuilder;
}

/**
* Populate a collection. This will return a new Querybuilder, allowing you to filter, join etc within it.
*
* @param {string} column
* @param {QueryBuilder} [queryBuilder]
* @param {string} [targetAlias]
*
* @returns {QueryBuilder<{new()}>}
*/
public populate(column: string, queryBuilder?: QueryBuilder<{new ()}>, targetAlias?: string): QueryBuilder<{new ()}> {
let {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.`);
}

let parentQueryBuilder = this.getChild(alias) || this;
let TargetReference = this.entityManager.resolveEntityReference(join.targetEntity);
targetAlias = targetAlias || parentQueryBuilder.createAlias(property);
parentQueryBuilder.mappings[targetAlias] = parentQueryBuilder.mappings[targetAlias] || Mapping.forEntity(TargetReference);

// Make sure we have a queryBuilder
if (!(queryBuilder instanceof QueryBuilder)) {
queryBuilder = this.entityManager.getRepository(TargetReference).getQueryBuilder(targetAlias);
}

let 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 {
// Make queryBuilder join with joinTable and figure out column...
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}`;

// Join from target to joinTable (treating target as owning side).
queryBuilder.join('innerJoin', joinTable.name, joinTableAlias, {
[`${targetAlias}.${joinColumn.referencedColumnName}`]: `${joinTableAlias}.${joinColumn.name}`
});
}

let hydrator = parentQueryBuilder.getHydrator();

hydrator.getRecipe().hydrate = true;

// No catalogue yet, ensure we at least fetch PK.
if (!hydrator.hasCatalogue(alias)) {
this.applyPrimaryKeySelect(alias);
}

return queryBuilder.setParent(property, parentColumn, hydrator.enableCatalogue(alias));
}

/**
* Get the relationship details for a column.
*
* @param {string} column
*
* @returns {{}}
*/
private getRelationship(column: string): {owningMapping: Mapping<Entity>, join: Relationship, property: string, alias: string} {
column = column.indexOf('.') > -1 ? column : `${this.alias}.${column}`;
let [alias, property] = column.split('.');
let parent = this.getChild(alias) || this;
let owningMapping = parent.mappings[alias];
let field;

// Ensure existing mapping
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 not found on entity, or relation not defined. ' +
'Are you registering the joins in the wrong order?'
);
}

return {owningMapping, join: field.relationship, property, alias};
}

/**
* Set the owner of this querybuilder.
*
* @param {string} property
* @param {string} column
* @param {Catalogue} catalogue
*
* @returns {QueryBuilder}
*/
public setParent(property: string, column: string, catalogue: Catalogue): this {
this.statement.select(`${column} as ${column}`);

this.query.setParent({column, primaries: catalogue.primaries});

this.hydrator.getRecipe().parent = {entities: catalogue.entities, column, property};

return this;
}

/** /**
* Get the Query. * Get the Query.
* *
Expand All @@ -353,13 +510,49 @@ export class QueryBuilder<T> {
* @returns {QueryBuilder} * @returns {QueryBuilder}
*/ */
public select(alias: Array<string> | string | {[key: string]: string}): this { public select(alias: Array<string> | string | {[key: string]: string}): this {
this.selects.push(alias); this.selects.push(...arguments);


this.prepared = false; this.prepared = false;


return this; return this;
} }


/**
* Get the alias of the parent.
*
* @returns {string}
*/
public getAlias(): string {
return this.alias;
}

/**
* Get the statement being built.
*
* @returns {knex.QueryBuilder}
*/
public getStatement(): knex.QueryBuilder {
return this.statement;
}

/**
* Get the mapping of the top-most Entity.
*
* @returns {Mapping<Entity>}
*/
public getHostMapping(): Mapping<Entity> {
return this.mappings[this.alias];
}

/**
* Get the hydrator of the query builder.
*
* @returns {Hydrator}
*/
public getHydrator(): Hydrator {
return this.hydrator;
}

/** /**
* Make sure all changes have been applied to the query. * Make sure all changes have been applied to the query.
* *
Expand Down Expand Up @@ -447,7 +640,6 @@ export class QueryBuilder<T> {
propertyAlias = `${alias}.${propertyAlias}`; propertyAlias = `${alias}.${propertyAlias}`;
} }


let aliasRecipe;
let selectAliases = []; let selectAliases = [];
let hydrateColumns = {}; let hydrateColumns = {};


Expand All @@ -457,13 +649,8 @@ export class QueryBuilder<T> {
let column = this.whereCriteria.mapToColumn(propertyAlias); let column = this.whereCriteria.mapToColumn(propertyAlias);
hydrateColumns[column] = property; hydrateColumns[column] = property;
alias = parts[0]; alias = parts[0];
aliasRecipe = this.hydrator.getRecipe(alias);


if (!this.appliedPrimaryKeys[alias]) { this.applyPrimaryKeySelect(alias);
this.appliedPrimaryKeys[alias] = `${aliasRecipe.primaryKey.alias} as ${aliasRecipe.primaryKey.alias}`;

selectAliases.push(this.appliedPrimaryKeys[alias]);
}


selectAliases.push(`${column} as ${column}`); selectAliases.push(`${column} as ${column}`);
} else { } else {
Expand Down Expand Up @@ -496,6 +683,24 @@ export class QueryBuilder<T> {
return this; return this;
} }


/**
* Ensure the existence of a primary key in the select of the query.
*
* @param {string} alias
*
* @returns {QueryBuilder}
*/
private applyPrimaryKeySelect(alias: string): this {
if (!this.appliedPrimaryKeys[alias]) {
let aliasRecipe = this.hydrator.getRecipe(alias);
this.appliedPrimaryKeys[alias] = `${aliasRecipe.primaryKey.alias} as ${aliasRecipe.primaryKey.alias}`;

this.statement.select(this.appliedPrimaryKeys[alias]);
}

return this;
}

/** /**
* Signal an insert. * Signal an insert.
* *
Expand Down

0 comments on commit cdf6880

Please sign in to comment.