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

Support multiple levels of relations #91

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
62 changes: 31 additions & 31 deletions lib/FindQueryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ class FindQueryBuilder {
*/
this._modelClass = modelClass;

/**
* A dictionary containing aliases for all the joins used.
*
* @type {Object}
* @private
*/
this._aliases = {};

/**
* If this is true (default) all property references are allowed.
*
Expand Down Expand Up @@ -134,7 +142,6 @@ class FindQueryBuilder {
* @type {Object.<string, function>}
* @private
*/

this._filters = Object.create(null);

/**
Expand Down Expand Up @@ -368,7 +375,6 @@ class FindQueryBuilder {
});
});
}

return parsed;
}

Expand All @@ -380,33 +386,26 @@ class FindQueryBuilder {
}

_buildJoins(params, builder) {
// Array of Objection `Relation` subclass instances.
const relationsToJoin = [];

_.each(params, function (param) {
_.each(params, (param) => {
_.each(param.propertyRefs, (ref) => {
const rel = ref.relation;
if (rel && rel.isOneToOne()) {
relationsToJoin.push(rel);
}
});
});

_.each(_.uniq(relationsToJoin, 'name'), (relation) => {
relation.join(builder, {
joinOperation: 'leftJoin',
relatedTableAlias: this._modelClass.tableName + '_rel_' + relation.name,
_.each(ref.relations, (rel) => {
if (!this._aliases[rel.relatedModelClass.tableName]) {
this._aliases[rel.relatedModelClass.tableName] = rel.relatedModelClass.tableName + '_alias';
rel.join(builder, {
joinOperation: 'innerJoin',
relatedTableAlias: this._aliases[rel.relatedModelClass.tableName],
ownerTable:
this._aliases[rel.ownerModelClass.tableName] || rel.ownerModelClass.tableName,
});
}
});
});
});

if (!_.isEmpty(relationsToJoin)) {
builder.select(this._modelClass.tableName + '.*');
}
builder.select(this._modelClass.tableName + '.*');
}

_buildFilters(params, builder) {
const filterParams = _.filter(params, 'filter');

const filterParams = _.filter(params, 'filter');
_.each(filterParams, (param) => {
this._buildFilter(param, builder);
});
Expand All @@ -420,12 +419,12 @@ class FindQueryBuilder {

if (refNames.length === 1) {
const ref = param.propertyRefs[refNames[0]];

ref.buildFilter(param, builder);
} else {
// If there are multiple property refs, they are combined with an `OR` operator.
builder.where(function () {
const builder = this;

_.each(param.propertyRefs, function (ref) {
ref.buildFilter(param, builder, 'or');
});
Expand All @@ -438,18 +437,20 @@ class FindQueryBuilder {
*/
_buildGroupBy(params, builder) {
const groupByParam = _.find(params, { key: 'groupBy' });
const joinParam = _.find(builder._operations, { name: 'innerJoin' });
const idColumn = this._modelClass.idColumn;

if (groupByParam) {
builder.select(groupByParam.value.split(','));
builder.groupBy(groupByParam.value.split(','));
} else if (joinParam) {
builder.groupBy(Array.isArray(idColumn) ? idColumn : idColumn.split(','));
}
}

/**
* @private
*/
_buildOrderBy(params, builder) {
const self = this;

_.each(params, (param) => {
const orderType = param.specialParameter;
let dir = 'asc';
Expand All @@ -461,16 +462,15 @@ class FindQueryBuilder {
dir = 'desc';
}

const rel = propertyRef.relation;
const rel = propertyRef.relations[0];
if (rel) {
if (!rel.isOneToOne()) {
utils.throwError(
"Can only order by model's own properties and by BelongsToOneRelation relations' properties"
);
}
const columnNameAlias = rel.name + _.capitalize(propertyRef.propertyName);
builder.select(propertyRef.fullColumnName() + ' as ' + columnNameAlias);
builder.orderBy(columnNameAlias, dir);

builder.orderBy(this._aliases[rel.name] + '.' + propertyRef.propertyName, dir);
} else {
builder.orderBy(propertyRef.columnName, dir);
}
Expand Down
95 changes: 41 additions & 54 deletions lib/PropertyRef.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const _ = require('lodash');
const utils = require('./utils');
const filters = require('./filters');

/**
* Instances of this class represent property references.
Expand Down Expand Up @@ -34,17 +33,24 @@ class PropertyRef {
*
* @type {Model}
*/
this.modelClass = null;
this._modelClass = null;

/**
* The relation part of the reference.
* This property reference refers to a property array of model.
*
* This is null for property references like `firstName`
* @type {Model[]}
*/
this.modelClasses = [];

/**
* The relations part of the reference.
*
* This is empty for property references like `firstName`
* that don't have the relation part.
*
* @type {Relation}
* @type {Relation[]}
*/
this.relation = null;
this.relations = [];

/**
* The name of the property this reference refers to.
Expand All @@ -67,32 +73,29 @@ class PropertyRef {

this._parse(str, builder);
}

_parse(str, builder) {
const parts = str.split('.');

if (parts.length === 1) {
this.propertyName = parts[0];
this.modelClass = builder._modelClass;
} else if (parts.length === 2) {
const relationName = parts[0];

try {
this.relation = builder._modelClass.getRelation(relationName);
} catch (err) {
utils.throwError('PropertyRef: unknown relation "' + relationName + '"');
this._modelClass = builder._modelClass;
this.columnName = this._modelClass.propertyNameToColumnName(this.propertyName);
if (!this.columnName) {
utils.throwError('PropertyRef: unknown property ' + str);
}

this.propertyName = parts[1];
this.modelClass = this.relation.relatedModelClass;
} else {
utils.throwError('PropertyRef: only one level of relations is supported');
}

this.columnName = this.modelClass.propertyNameToColumnName(this.propertyName);

if (!this.columnName) {
utils.throwError('PropertyRef: unknown property ' + str);
} else if (parts.length >= 2) {
this.propertyName = parts.pop();
this._modelClass = builder._modelClass;
let prevParent = builder._modelClass;
for (let index = 0; index < parts.length; index++) {
try {
this.relations.push(prevParent.getRelation(parts[index]));
prevParent = this.relations[index].relatedProp._modelClass;
this.modelClasses.push(prevParent);
} catch (err) {
utils.throwError(`PropertyRef: unknown relation ${parts[index]}`);
}
}
this.columnName = this.modelClasses[0].propertyNameToColumnName(this.propertyName);
}
}

Expand All @@ -105,51 +108,35 @@ class PropertyRef {
* @returns {string}
*/
fullColumnName() {
if (this.relation && this.relation.isOneToOne()) {
const builder = this.modelClass.query();
// one-to-one relations are joined and the joined table is given an alias.
// We must refer to the column through that alias.

return (
this.relation.ownerModelClass.getTableName() +
'_rel_' +
this.relation.name +
'.' +
this.columnName
);
const relation = this.relations.at(-1);
if (relation) {
const modelClass = relation.relatedModelClass;
return modelClass.tableName + '_alias.' + this.columnName;
} else {
return this.modelClass.tableName + '.' + this.columnName;
return this._modelClass.tableName + '.' + this.columnName;
}
}

/**
* Builds a where statement.
* Converts filters to where queries.
*
* @param {QueryParameter} param
* @param {QueryBuilder} builder
* @param {string=} boolOp
*/
buildFilter(param, builder, boolOp) {
const filter = this._getFilter(param);
let whereMethod = filter.method;
const filter = this._getFilter(param, this._modelClass);

let whereMethod = filter.method;
if (boolOp) {
whereMethod = boolOp + _.upperFirst(whereMethod);
}

const rel = this.relation;
if (rel && !rel.isOneToOne()) {
const subQuery = rel.ownerModelClass.relatedQuery(rel.name).alias(rel.relatedModelClass.name);
subQuery[whereMethod].apply(subQuery, filter.args);

builder.whereExists(subQuery.select(1));
} else {
builder[whereMethod].apply(builder, filter.args);
}
builder[whereMethod].apply(builder, filter.args);
}

_getFilter(param) {
return param.filter(this, param.value, this.modelClass);
_getFilter(param, modelClass) {
return param.filter(this, param.value, modelClass);
}
}

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.