Skip to content
This repository has been archived by the owner on Mar 1, 2022. It is now read-only.

Commit

Permalink
✨ Added support for filtering by multiple relation columns
Browse files Browse the repository at this point in the history
closes #14

- need to add more context (!)
  • Loading branch information
kirrg001 committed Nov 14, 2018
1 parent 771cc0b commit f0a0f4c
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 42 deletions.
115 changes: 81 additions & 34 deletions lib/convertor.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,30 @@ class MongoToKnex {
debug(columnParts);

const table = columnParts[0];
const relation = this.config.relations[table];
let relation = this.config.relations[table];

if (!relation) {
throw new Error('Can\'t find relation in config object.');
relation = _.find(this.config.relations, (relation) => {
return relation.join_table === table;
});

if (!relation) {
throw new Error('Can\'t find relation in config object.');
}

return {
join_table: relation.join_table,
table: relation.tableName,
column: columnParts[1],
operator: op,
value: value,
config: relation,
isRelation: true
};
}

return {
table: columnParts[0],
column: columnParts[1],
operator: op,
value: value,
Expand All @@ -91,33 +108,45 @@ class MongoToKnex {
*
* - OR conjunctions for many-to-many relations
*/
buildRelationQuery(qb, relation) {
buildRelationQuery(qb, relations) {
debug(`(buildRelationQuery)`);

if (debugExtended.enabled) {
debugExtended(`(buildRelationQuery) ${JSON.stringify(relation)}`);
debugExtended(`(buildRelationQuery) ${JSON.stringify(relations)}`);
}

if (relation.config.type === 'manyToMany') {
if (isCompOp(relation.operator)) {
const comp = compOps[relation.operator] || '=';

// CASE: post.id IN (SELECT ...)
qb.where(`${this.tableName}.id`, 'IN', function () {
return this
.select(`${relation.config.join_table}.${relation.config.join_from}`)
.from(`${relation.config.join_table}`)
.innerJoin(`${relation.config.tableName}`, `${relation.config.tableName}.id`, '=', `${relation.config.join_table}.${relation.config.join_to}`)
.where(`${relation.config.tableName}.${relation.column}`, comp, relation.value);
});
} else {
debug('unknown operator');
}
const groupedRelations = _.groupBy(relations, 'table');

return;
}
_.each(Object.keys(groupedRelations), (key) => {
debug(`(buildRelationQuery) build relation for ${key}`);

const statements = groupedRelations[key];
const ref = statements[0];

if (ref.config.type === 'manyToMany') {
if (isCompOp(ref.operator)) {
const comp = compOps[ref.operator] || '=';

// CASE: post.id IN (SELECT ...)
qb.where(`${this.tableName}.id`, 'IN', function () {
const innerQB = this
.select(`${ref.config.join_table}.${ref.config.join_from}`)
.from(`${ref.config.join_table}`)
.innerJoin(`${ref.config.tableName}`, `${ref.config.tableName}.id`, '=', `${ref.config.join_table}.${ref.config.join_to}`);

debug('not implemented');
_.each(statements, (value, key) => {
debug(`(buildRelationQuery) build relation where statements for ${key}`);

innerQB.where(`${value.join_table || value.table}.${value.column}`, comp, value.value);
});

return innerQB;
});
} else {
debug('unknown operator');
}
}
});
}

/**
Expand All @@ -129,15 +158,25 @@ class MongoToKnex {
* `where column != value`
* `where column > value`
*/
buildComparison(qb, mode, statement, op, value) {
buildComparison(qb, mode, statement, op, value, group) {
const comp = compOps[op] || '=';
const whereType = this.processWhereType(mode, op, value);
const processedStatement = this.processStatement(statement, op, value);

debug(`(buildComparison) isRelation: ${processedStatement.isRelation}`);
debug(`(buildComparison) isRelation: ${processedStatement.isRelation}, group: ${group}`);

if (processedStatement.isRelation) {
return this.buildRelationQuery(qb, processedStatement);
if (!group) {
this.buildRelationQuery(qb, [processedStatement]);
return;
}

if (!qb.hasOwnProperty('relations')) {
qb.relations = [];
}

qb.relations.push(processedStatement);
return;
}

debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value}`);
Expand All @@ -147,7 +186,7 @@ class MongoToKnex {
/**
* {author: 'carl'}
*/
buildWhereClause(qb, mode, statement, sub) {
buildWhereClause(qb, mode, statement, sub, group) {
debug(`(buildWhereClause) mode: ${mode}, statement: ${statement}`);

if (debugExtended.enabled) {
Expand All @@ -156,13 +195,13 @@ class MongoToKnex {

// CASE sub is an atomic value, we use "eq" as default operator
if (!_.isObject(sub)) {
return this.buildComparison(qb, mode, statement, '$eq', sub);
return this.buildComparison(qb, mode, statement, '$eq', sub, group);
}

// CASE: sub is an object, contains statements and operators
_.forIn(sub, (value, op) => {
if (isCompOp(op)) {
this.buildComparison(qb, mode, statement, op, value);
this.buildComparison(qb, mode, statement, op, value, group);
} else {
debug('unknown operator');
}
Expand All @@ -171,6 +210,8 @@ class MongoToKnex {

/**
* {$and: [{author: 'carl'}, {status: 'draft'}]}}
* {$and: {author: 'carl'}}
* {$and: {author: { $in: [...] }}}
*/
buildWhereGroup(qb, parentMode, mode, sub) {
const whereType = this.processWhereType(parentMode);
Expand All @@ -183,14 +224,19 @@ class MongoToKnex {

qb[whereType]((_qb) => {
if (_.isArray(sub)) {
sub.forEach(statement => this.buildQuery(_qb, mode, statement));
sub.forEach(statement => this.buildQuery(_qb, mode, statement, true));
} else if (_.isObject(sub)) {
this.buildQuery(_qb, mode, sub);
this.buildQuery(_qb, mode, sub, true);
}

if (_qb.hasOwnProperty('relations')) {
this.buildRelationQuery(_qb, _qb.relations);
delete _qb.relations;
}
});
}

buildQuery(qb, mode, sub) {
buildQuery(qb, mode, sub, group) {
debug(`(buildQuery) mode: ${mode}`);

if (debugExtended.enabled) {
Expand All @@ -201,9 +247,10 @@ class MongoToKnex {
debug(`(buildQuery) key: ${key}`);

if (isLogicOp(key)) {
// CASE: you have two groups ($or), you have one group ($and)
this.buildWhereGroup(qb, mode, key, value);
} else {
this.buildWhereClause(qb, mode, key, value);
this.buildWhereClause(qb, mode, key, value, group);
}
});
}
Expand All @@ -221,8 +268,8 @@ class MongoToKnex {
debugExtended(`(processJSON) ${JSON.stringify(mongoJSON)}`);
}

// And is the default behaviour
this.buildQuery(qb, 'and', mongoJSON);
// 'and' is the default behaviour
this.buildQuery(qb, '$and', mongoJSON);
}
}

Expand Down
101 changes: 101 additions & 0 deletions test/integration/relations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const makeQuery = query => convertor(knex('posts'), query, {
// Integration tests build a test database and
// check that we get the exact data we expect from each query
describe('Relations', function () {
before(utils.db.teardown());
before(utils.db.setup());
after(utils.db.teardown());

Expand Down Expand Up @@ -86,6 +87,106 @@ describe('Relations', function () {
describe('Many-to-Many', function () {
before(utils.db.init('relations1'));

describe('EQUALS', function () {
it('filter by relation column', function () {
const mongoJSON = {
'tags.slug': 'animal'
};

const query = makeQuery(mongoJSON);

return query
.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(3);
});
});
});

describe('Multiple where clauses for relations', function () {
it('combine $or and $and', function () {
// where primary tag is "animal"
const mongoJSON = {$and: [
{
$and: [
{
'tags.slug': 'animal'
},
{
'posts_tags.sort_order': 0
}
]

},
{
featured: true
}
]};

const query = makeQuery(mongoJSON);

return query
.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(0);
});
});

it('combine $or and $and', function () {
// where primary tag is "animal"
const mongoJSON = {$and: [
{
$and: [
{
'tags.slug': 'animal'
},
{
'posts_tags.sort_order': 0
}
]
},
{
featured: false
}
]};

const query = makeQuery(mongoJSON);

return query
.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(1);
});
});

it('combine $or and $and', function () {
const mongoJSON = {$or: [
{
$and: [
{
'tags.slug': 'animal'
},
{
'posts_tags.sort_order': 0
},
{
featured: false
}
]
},
{author_id: 1}
]};

const query = makeQuery(mongoJSON);

return query
.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(5);
});
});
});

describe('OR', function () {
it('tags.slug IN (animal)', function () {
const mongoJSON = {
Expand Down
16 changes: 8 additions & 8 deletions test/integration/suite1/fixtures/relations1.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"posts_tags": [
{"post_id": 1, "tag_id": 1},
{"post_id": 2, "tag_id": 2},
{"post_id": 3, "tag_id": 3},
{"post_id": 4, "tag_id": 1},
{"post_id": 4, "tag_id": 2},
{"post_id": 5, "tag_id": 1},
{"post_id": 6, "tag_id": 1},
{"post_id": 6, "tag_id": 2}
{"post_id": 1, "tag_id": 1, "sort_order": 0},
{"post_id": 2, "tag_id": 2, "sort_order": 0},
{"post_id": 3, "tag_id": 3, "sort_order": 0},
{"post_id": 4, "tag_id": 1, "sort_order": 0},
{"post_id": 4, "tag_id": 2, "sort_order": 1},
{"post_id": 5, "tag_id": 1, "sort_order": 0},
{"post_id": 6, "tag_id": 1, "sort_order": 0},
{"post_id": 6, "tag_id": 2, "sort_order": 1}
]
}

0 comments on commit f0a0f4c

Please sign in to comment.