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

✨Added support for relations: OR conjunction cases #12

Merged
merged 4 commits into from
Nov 13, 2018
Merged
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
128 changes: 99 additions & 29 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,60 +40,130 @@ class MongoToKnex {
return 'andWhere';
}

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

// CASE: `posts.status` -> where "posts" is the parent table
if (columnParts[0] === this.tableName) {
return {
column: column,
value: value,
isRelation: false
};
}

// CASE: relation?
if (columnParts.length > 1) {
debug(columnParts);

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});
const table = columnParts[0];
const relation = this.config.relations[table];

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

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

return this.tableName + '.' + field;
// CASE: fallback, `status=draft` -> `posts.status`=draft
return {
column: this.tableName + '.' + column,
value: value,
isRelation: false
};
}

/**
* Simple comparison on the parent table.
* @TODO: This implementation serves currently only one use case:
*
* - OR conjunctions for many-to-many relations
*/
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');
}

/**
* Determines if statement is a simple where comparison on the parent table or if the statement is a relation query.
*
* e.g.
*
* `where column = value`
* `where column != value`
* `where column > value`
*/
buildComparison(qb, mode, field, op, value) {
buildComparison(qb, mode, statement, op, value) {
const comp = compOps[op] || '=';
const whereType = this.processWhereType(mode, op, value);
field = this.processField(field, op);
const processedStatement = this.processStatement(statement, op, value);

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

debug(`(buildComparison) whereType: ${whereType}, field: ${field}, op: ${op}, comp: ${comp}, value: ${value}`);
qb[whereType](field, comp, value);
if (processedStatement.isRelation) {
return this.buildRelationQuery(qb, processedStatement);
}

debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value}`);
qb[whereType](processedStatement.column, comp, processedStatement.value);
}

/**
* {author: 'carl'}
*/
buildWhereClause(qb, mode, field, sub) {
debug(`(buildWhereClause) mode: ${mode}, field: ${field}`);
buildWhereClause(qb, mode, statement, sub) {
debug(`(buildWhereClause) mode: ${mode}, statement: ${statement}`);

if (debugExtended.enabled) {
debugExtended(`(buildWhereClause) ${JSON.stringify(sub)}`);
}

// CASE sub is an atomic value, we use "eq" as default operator
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);
}
});
return this.buildComparison(qb, mode, statement, '$eq', sub);
}

// CASE: sub is an object, contains statements and operators
_.forIn(sub, (value, op) => {
if (isCompOp(op)) {
this.buildComparison(qb, mode, statement, op, value);
} else {
debug('unknown operator');
}
});
}

/**
Expand Down Expand Up @@ -153,10 +223,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
137 changes: 58 additions & 79 deletions test/integration/relations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ const convertor = require('../../lib/convertor');

/* eslint-disable no-console*/

// @TODO: the config object is not designed yet.
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 +26,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 +83,64 @@ 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']}};

// Use the queryJSON to build a query
const query = makeQuery(queryJSON);

// Check any intermediate values
console.log(query.toQuery());

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

result.should.be.an.Array().with.lengthOf(3);

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

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

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

// Check any intermediate values
console.log('query', query.toQuery());

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

result.should.be.an.Array().with.lengthOf(4);
describe('Many-to-Many', function () {
before(utils.db.init('relations1'));

// Check we get the right data
// result.should.do.something;
describe('OR', function () {
it('tags.slug IN (animal)', function () {
const mongoJSON = {
'tags.slug': {
$in: ['animal']
}
};

done();
})
.catch(done);
});
const query = makeQuery(mongoJSON);

it('can match array in (multiple values with x, y and xy)', function (done) {
const queryJSON = {'tags.id': {$in: [1, 2]}};
return query
.select()
.then((result) => {
result.should.be.an.Array().with.lengthOf(3);
});
});

// Use the queryJSON to build a query
const query = makeQuery(queryJSON);
it('featured:true AND tags.slug IN (animal)', function () {
const mongoJSON = {
$and: [
{
featured: true
},
{
'tags.slug': {
$in: ['animal']
}
}
]
};

// Check any intermediate values
console.log('query', query.toQuery());
const query = makeQuery(mongoJSON);

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

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

// Check we get the right data
// result.should.do.something;
const query = makeQuery(mongoJSON);

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

Expand Down
24 changes: 0 additions & 24 deletions test/unit/convertor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,27 +282,3 @@ describe('Logical Groups', function () {
});
});
});

describe('Relations', function () {
describe('IN with array of objects', function () {
it('can match array in (single value)', function () {
runQuery({'tags.slug': {$in: ['video']}})
.should.eql('select * from `posts` where `tags`.`slug` in (\'video\')');
});

it('can match array in (multiple values)', function () {
runQuery({'tags.slug': {$in: ['video', 'audio']}})
.should.eql('select * from `posts` where `tags`.`slug` in (\'video\', \'audio\')');
});

it('can match array NOT in (single value)', function () {
runQuery({'tags.slug': {$nin: ['video']}})
.should.eql('select * from `posts` where `tags`.`slug` not in (\'video\')');
});

it('can match array NOT in (multiple values)', function () {
runQuery({'tags.slug': {$nin: ['video', 'audio']}})
.should.eql('select * from `posts` where `tags`.`slug` not in (\'video\', \'audio\')');
});
});
});
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