Skip to content

Commit

Permalink
Added support for relations: OR conjunction cases
Browse files Browse the repository at this point in the history
  • Loading branch information
kirrg001 committed Nov 9, 2018
1 parent e9f4b10 commit b6a1b87
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 87 deletions.
104 changes: 88 additions & 16 deletions lib/convertor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const isLogicOp = key => isOp(key) && _.includes(logicOps, key);
const isCompOp = key => isOp(key) && _.includes(_.keys(compOps), key);

class MongoToKnex {
constructor(options = {}) {
constructor(options = {}, config = {}) {
this.tableName = options.tableName;
this.joins = [];
this.config = config;
}

processWhereType(mode, op, value) {
Expand All @@ -40,21 +40,83 @@ class MongoToKnex {
return 'andWhere';
}

processField(field, op) {
/**
* Determine if field lives on parent table or on a relation.
*/
processField(field, op, value) {
const fieldParts = field.split('.');

// CASE: `posts.status`?
if (fieldParts[0] === this.tableName) {
// If we have the right table already, return
return field;
} else if (fieldParts.length > 1) {
// If we have a different table, that should be a join
// Store the OP because an IN is different
this.joins.push({table: fieldParts[0], op});

return field;
return {
field: field,
isRelation: false
};
}

return this.tableName + '.' + field;
// CASE: relation?
if (fieldParts.length > 1) {
debug(fieldParts);

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

if (!relation) {
debug('Can\'t find relation in config object.');

return {
field: field,
isRelation: false
};
}

return {
table: table,
column: fieldParts[1],
operator: op,
value: value,
config: relation,
isRelation: true
};
}

// CASE: fallback, `status=draft`?
return {
field: this.tableName + '.' + field,
isRelation: false
};
}

/**
* @TODO: This implementation serves currently only one use case.
*/
buildRelationQuery(qb, relation) {
debug(`(buildRelationQuery)`);

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

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');
}

return;
}

debug('not implemented');
}

/**
Expand All @@ -69,10 +131,16 @@ class MongoToKnex {
buildComparison(qb, mode, field, op, value) {
const comp = compOps[op] || '=';
const whereType = this.processWhereType(mode, op, value);
field = this.processField(field, op);
const processedField = this.processField(field, op, value);

debug(`(buildComparison) isRelation: ${processedField.isRelation}`);

if (processedField.isRelation) {
return this.buildRelationQuery(qb, processedField);
}

debug(`(buildComparison) whereType: ${whereType}, field: ${field}, op: ${op}, comp: ${comp}, value: ${value}`);
qb[whereType](field, comp, value);
qb[whereType](processedField.field, comp, value);
}

/**
Expand All @@ -85,12 +153,16 @@ class MongoToKnex {
debugExtended(`(buildWhereClause) ${JSON.stringify(sub)}`);
}

// CASE 1: sub is a value, we use "eq" as operator
// CASE 2: sub is an object, contains fields and operators
if (!_.isObject(sub)) {
this.buildComparison(qb, mode, field, '$eq', sub);
} else {
_.forIn(sub, (value, op) => {
if (isCompOp(op)) {
this.buildComparison(qb, mode, field, op, value);
} else {
debug('unknown operator');
}
});
}
Expand Down Expand Up @@ -153,10 +225,10 @@ class MongoToKnex {
}
}

module.exports = function convertor(qb, mongoJSON) {
module.exports = function convertor(qb, mongoJSON, config) {
const mongoToKnex = new MongoToKnex({
tableName: qb._single.table
});
}, config);

mongoToKnex.processJSON(qb, mongoJSON);

Expand Down
135 changes: 67 additions & 68 deletions test/integration/relations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ const convertor = require('../../lib/convertor');
const makeQuery = query => convertor(knex('posts'), query, {
relations: {
tags: {
tableName: 'tags',
type: 'manyToMany',
join_table: 'posts_tags',
join_from: 'post_id',
join_to: 'tag_id'
join_to: 'tag_id',
// @TODO: tag -> tags.slug
aliases: {}
},
authors: {
type: 'oneToMany',
Expand All @@ -22,19 +25,12 @@ 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.skip('Relations', function () {
before(utils.db.setup(() => {
// Do things afterwards in a callback
}));

describe('Relations', function () {
before(utils.db.setup());
after(utils.db.teardown());

describe('One-to-Many', function () {
// Use a named file
beforeEach(utils.db.init('test-fixture', () => {
// could do stuff after
}));

describe.skip('One-to-Many', function () {
beforeEach(utils.db.init('relations1'));
afterEach(utils.db.reset());

it('can match array in (single value)', function (done) {
Expand Down Expand Up @@ -86,82 +82,85 @@ describe.skip('Relations', function () {
});
});

describe('Many-to-Many: Simple Cases', function () {
// Use the default file named after the describe block, would be many-to-many-simple-cases
beforeEach(utils.db.init());

afterEach(utils.db.reset());

it('can match array in (single value)', function (done) {
const queryJSON = {'tags.slug': {$in: ['animal']}};
describe('Many-to-Many', function () {
before(utils.db.init('relations1'));

// Use the queryJSON to build a query
const query = makeQuery(queryJSON);
describe('OR', function () {
it('tags.slug IN (animal)', function (done) {
const queryJSON = {'tags.slug': {$in: ['animal']}};

// Check any intermediate values
console.log(query.toQuery());
// Use the queryJSON to build a query
const query = makeQuery(queryJSON);

// Perform the query against the DB
query.select()
.then((result) => {
console.log(result);
// Perform the query against the DB
query.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(3);

result.should.be.an.Array().with.lengthOf(3);
// Check we get the right data
// result.should.do.something;

// Check we get the right data
// result.should.do.something;
done();
})
.catch(done);
});

done();
})
.catch(done);
});
it('featured:true AND tags.slug IN (animal)', function (done) {
const queryJSON = {$and: [{featured: true}, {'tags.slug': {$in: ['animal']}}]};

it('can match array in (multiple values)', function (done) {
const queryJSON = {'tags.id': {$in: [2, 3]}};
// Use the queryJSON to build a query
const query = makeQuery(queryJSON);

// Use the queryJSON to build a query
const query = makeQuery(queryJSON);
// Perform the query against the DB
query.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(2);

// Check any intermediate values
console.log('query', query.toQuery());
// Check we get the right data
// result.should.do.something;

// Perform the query against the DB
query.select()
.then((result) => {
console.log(result);
done();
})
.catch(done);
});

result.should.be.an.Array().with.lengthOf(4);
it('tags.id IN (2,3)', function (done) {
const queryJSON = {'tags.id': {$in: [2, 3]}};

// Check we get the right data
// result.should.do.something;
// Use the queryJSON to build a query
const query = makeQuery(queryJSON);

done();
})
.catch(done);
});
// Perform the query against the DB
query.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(4);

it('can match array in (multiple values with x, y and xy)', function (done) {
const queryJSON = {'tags.id': {$in: [1, 2]}};
// Check we get the right data
// result.should.do.something;

// Use the queryJSON to build a query
const query = makeQuery(queryJSON);
done();
})
.catch(done);
});

// Check any intermediate values
console.log('query', query.toQuery());
it('tags.id IN (1,2)', function (done) {
const queryJSON = {'tags.id': {$in: [1, 2]}};

// Perform the query against the DB
query.select()
.then((result) => {
console.log(result);
// Use the queryJSON to build a query
const query = makeQuery(queryJSON);

result.should.be.an.Array().with.lengthOf(5);
// Perform the query against the DB
query.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(5);

// Check we get the right data
// result.should.do.something;
// Check we get the right data
// result.should.do.something;

done();
})
.catch(done);
done();
})
.catch(done);
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/unit/convertor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describe('Logical Groups', function () {
});
});

describe('Relations', function () {
describe.skip('Relations', function () {
describe('IN with array of objects', function () {
it('can match array in (single value)', function () {
runQuery({'tags.slug': {$in: ['video']}})
Expand Down
5 changes: 3 additions & 2 deletions test/utils/db/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ module.exports.setup = name => function innerSetup() {
* `init('fixtures.json')`
*/
module.exports.init = (suiteName, fixtureFileName) => {
if (!suiteName) {
suiteName = fixtureFileName;
if (!fixtureFileName) {
fixtureFileName = suiteName;
suiteName = null;
}

return function innerInit() {
Expand Down

0 comments on commit b6a1b87

Please sign in to comment.