Skip to content
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
55 changes: 36 additions & 19 deletions packages/mongo-knex/lib/convertor.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const isLogicOp = key => isOp(key) && _.includes(logicOps, key);
const isCompOp = key => isOp(key) && _.includes(_.keys(compOps), key);
const isNegationOp = key => isOp(key) && _.includes(['$ne', '$nin'], key);
const isStatementGroupOp = key => _.includes([compOps.$in, compOps.$nin], key);
const isLikeOp = key => isOp(key) && _.includes(['$regex', '$not'], key);

/**
* JSON Stringify with RegExp support
Expand Down Expand Up @@ -280,7 +281,7 @@ class MongoToKnex {
const whereType = ['whereNull', 'whereNotNull'].includes(reference.whereType) ? 'andWhere' : (['orWhereNull', 'orWhereNotNull'].includes(reference.whereType) ? 'orWhere' : reference.whereType);

// CASE: WHERE resource.id (IN | NOT IN) (SELECT ...)
qb[whereType](`${this.tableName}.id`, comp, function () {
qb[whereType](`${this.tableName}.id`, comp, (whereQb) => {
const joinFilterStatements = groupedRelations[key].joinFilterStatements;

let innerJoinValue = reference.config.tableName;
Expand All @@ -294,7 +295,7 @@ class MongoToKnex {

const joinType = reference.config.joinType || 'innerJoin';

const innerQB = this
const innerQB = whereQb
.select(`${reference.config.joinTable}.${reference.config.joinFrom}`)
.from(`${reference.config.joinTable}`)[joinType](innerJoinValue, function () {
this.on(innerJoinOn, '=', `${reference.config.joinTable}.${reference.config.joinTo}`);
Expand All @@ -312,10 +313,15 @@ class MongoToKnex {

_.each(statements, (statement, _key) => {
debug(`(buildRelationQuery) build relation where statements for ${_key}`);

const statementColumn = `${statement.joinTable || statement.table}.${statement.column}`;
let statementOp;

// CASE: LIKE query --> use the existing builder
if (isLikeOp(statement.operator)) {
this.buildLikeComparison(innerQB, {...statement, column: statementColumn}, statement.whereType);
return;
}

if (negateGroup) {
statementOp = compOps.$in;
} else {
Expand Down Expand Up @@ -359,7 +365,7 @@ class MongoToKnex {
const tableName = this.tableName;

const where = reference.whereType === 'orWhere' ? 'orWhere' : 'where';
qb[where](`${this.tableName}.id`, comp, function () {
qb[where](`${this.tableName}.id`, comp, (whereQb) => {
const joinFilterStatements = groupedRelations[key].joinFilterStatements;

let innerJoinValue = reference.config.tableName;
Expand All @@ -371,7 +377,7 @@ class MongoToKnex {
innerJoinOn = `${reference.config.tableNameAs}.${reference.config.joinFrom}`;
}

const innerQB = this
const innerQB = whereQb
.select(`${tableName}.id`)
.from(`${tableName}`)
.leftJoin(innerJoinValue, function () {
Expand All @@ -390,6 +396,12 @@ class MongoToKnex {
const statementColumn = `${statement.table}.${statement.column}`;
let statementOp;

// CASE: LIKE query --> use the existing builder
if (isLikeOp(statement.operator)) {
this.buildLikeComparison(innerQB, {...statement, column: statementColumn}, statement.whereType);
return;
}

// NOTE: this negation is here to ensure records with no relation are
// include in negation (e.g. `relation.columnName: {$ne: null})
if (negateGroup) {
Expand Down Expand Up @@ -469,27 +481,32 @@ class MongoToKnex {
op = processedStatement.operator;
value = processedStatement.value;

if (op === '$regex' || op === '$not') {
const {source, ignoreCase} = processRegExp(value);
value = source;

// CASE: regex with i flag needs whereRaw to wrap column in lower() else fall through
if (ignoreCase) {
whereType += 'Raw';
debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value} (REGEX/i)`);
qb[whereType](`lower(??) ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
return;
}
whereType += 'Raw';
debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value} (REGEX)`);
qb[whereType](`?? ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
if (isLikeOp(op)) {
this.buildLikeComparison(qb, processedStatement, whereType);
return;
}

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

buildLikeComparison(qb, {column, operator: op, value}, whereType) {
const comp = compOps[op] || '=';
const {source, ignoreCase} = processRegExp(value);
value = source;

// CASE: regex with i flag needs whereRaw to wrap column in lower() else fall through
if (ignoreCase) {
whereType += 'Raw';
debug(`(buildLikeComparison) whereType: ${whereType}, op: ${op}, comp: ${comp}, value: ${value} (REGEX/i)`);
qb[whereType](`lower(??) ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
return;
}
whereType += 'Raw';
debug(`(buildLikeComparison) whereType: ${whereType}, op: ${op}, comp: ${comp}, value: ${value} (REGEX)`);
qb[whereType](`?? ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
}

/**
* {author: 'carl'}
*/
Expand Down
24 changes: 24 additions & 0 deletions packages/mongo-knex/test/unit/convertor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,30 @@ describe('Relations', function () {
runQuery({'posts_meta.meta_title': {$ne: 'Meta of A Whole New World'}})
.should.eql('select * from `posts` where `posts`.`id` not in (select `posts`.`id` from `posts` left join `posts_meta` on `posts_meta`.`post_id` = `posts`.`id` where `posts_meta`.`meta_title` in (\'Meta of A Whole New World\'))');
});

it('should be able to perform a match query on a one-to-one relation', function () {
const innerQuery = 'select `posts`.`id` from `posts` left join `posts_meta` on `posts_meta`.`post_id` = `posts`.`id` where lower(`posts_meta`.`meta_title`) like \'%world%\' ESCAPE \'*\'';
runQuery({'posts_meta.meta_title': {$regex: /world/i}})
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
});

it('should be able to perform a negated match query on a one-to-one relation', function () {
const innerQuery = 'select `posts`.`id` from `posts` left join `posts_meta` on `posts_meta`.`post_id` = `posts`.`id` where lower(`posts_meta`.`meta_title`) not like \'%world%\' ESCAPE \'*\'';
runQuery({'posts_meta.meta_title': {$not: /world/i}})
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
});

it('should be able to perform a match query on a many-to-many relation', function () {
const innerQuery = 'select `posts_tags`.`post_id` from `posts_tags` inner join `tags` on `tags`.`id` = `posts_tags`.`tag_id` where `tags`.`name` like \'%wor*%ld%\' ESCAPE \'*\'';
runQuery({'tags.name': {$regex: /wor%ld/}})
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
});

it('should be able to perform a negated match query on a many-to-many relation', function () {
const innerQuery = 'select `posts_tags`.`post_id` from `posts_tags` inner join `tags` on `tags`.`id` = `posts_tags`.`tag_id` where `tags`.`name` not like \'%wor*%ld%\' ESCAPE \'*\'';
runQuery({'tags.name': {$not: /wor%ld/}})
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
});
});

describe('RegExp/Like queries', function () {
Expand Down