Skip to content

Commit cc25cfc

Browse files
🐛 added match support to nested relations
1 parent 6f4ddce commit cc25cfc

2 files changed

Lines changed: 60 additions & 19 deletions

File tree

packages/mongo-knex/lib/convertor.js

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const isLogicOp = key => isOp(key) && _.includes(logicOps, key);
2828
const isCompOp = key => isOp(key) && _.includes(_.keys(compOps), key);
2929
const isNegationOp = key => isOp(key) && _.includes(['$ne', '$nin'], key);
3030
const isStatementGroupOp = key => _.includes([compOps.$in, compOps.$nin], key);
31+
const isLikeOp = key => isOp(key) && _.includes(['$regex', '$not'], key);
3132

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

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

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

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

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

313314
_.each(statements, (statement, _key) => {
314315
debug(`(buildRelationQuery) build relation where statements for ${_key}`);
315-
316316
const statementColumn = `${statement.joinTable || statement.table}.${statement.column}`;
317317
let statementOp;
318318

319+
// CASE: LIKE query --> use the existing builder
320+
if (isLikeOp(statement.operator)) {
321+
this.buildLikeComparison(innerQB, {...statement, column: statementColumn}, statement.whereType);
322+
return;
323+
}
324+
319325
if (negateGroup) {
320326
statementOp = compOps.$in;
321327
} else {
@@ -359,7 +365,7 @@ class MongoToKnex {
359365
const tableName = this.tableName;
360366

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

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

374-
const innerQB = this
380+
const innerQB = whereQb
375381
.select(`${tableName}.id`)
376382
.from(`${tableName}`)
377383
.leftJoin(innerJoinValue, function () {
@@ -390,6 +396,12 @@ class MongoToKnex {
390396
const statementColumn = `${statement.table}.${statement.column}`;
391397
let statementOp;
392398

399+
// CASE: LIKE query --> use the existing builder
400+
if (isLikeOp(statement.operator)) {
401+
this.buildLikeComparison(innerQB, {...statement, column: statementColumn}, statement.whereType);
402+
return;
403+
}
404+
393405
// NOTE: this negation is here to ensure records with no relation are
394406
// include in negation (e.g. `relation.columnName: {$ne: null})
395407
if (negateGroup) {
@@ -469,27 +481,32 @@ class MongoToKnex {
469481
op = processedStatement.operator;
470482
value = processedStatement.value;
471483

472-
if (op === '$regex' || op === '$not') {
473-
const {source, ignoreCase} = processRegExp(value);
474-
value = source;
475-
476-
// CASE: regex with i flag needs whereRaw to wrap column in lower() else fall through
477-
if (ignoreCase) {
478-
whereType += 'Raw';
479-
debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value} (REGEX/i)`);
480-
qb[whereType](`lower(??) ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
481-
return;
482-
}
483-
whereType += 'Raw';
484-
debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value} (REGEX)`);
485-
qb[whereType](`?? ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
484+
if (isLikeOp(op)) {
485+
this.buildLikeComparison(qb, processedStatement, whereType);
486486
return;
487487
}
488488

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

493+
buildLikeComparison(qb, {column, operator: op, value}, whereType) {
494+
const comp = compOps[op] || '=';
495+
const {source, ignoreCase} = processRegExp(value);
496+
value = source;
497+
498+
// CASE: regex with i flag needs whereRaw to wrap column in lower() else fall through
499+
if (ignoreCase) {
500+
whereType += 'Raw';
501+
debug(`(buildLikeComparison) whereType: ${whereType}, op: ${op}, comp: ${comp}, value: ${value} (REGEX/i)`);
502+
qb[whereType](`lower(??) ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
503+
return;
504+
}
505+
whereType += 'Raw';
506+
debug(`(buildLikeComparison) whereType: ${whereType}, op: ${op}, comp: ${comp}, value: ${value} (REGEX)`);
507+
qb[whereType](`?? ${comp} ? ESCAPE ?`, [column, value, likeEscapeCharacter]);
508+
}
509+
493510
/**
494511
* {author: 'carl'}
495512
*/

packages/mongo-knex/test/unit/convertor.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,30 @@ describe('Relations', function () {
407407
runQuery({'posts_meta.meta_title': {$ne: 'Meta of A Whole New World'}})
408408
.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\'))');
409409
});
410+
411+
it('should be able to perform a match query on a one-to-one relation', function () {
412+
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 \'*\'';
413+
runQuery({'posts_meta.meta_title': {$regex: /world/i}})
414+
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
415+
});
416+
417+
it('should be able to perform a negated match query on a one-to-one relation', function () {
418+
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 \'*\'';
419+
runQuery({'posts_meta.meta_title': {$not: /world/i}})
420+
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
421+
});
422+
423+
it('should be able to perform a match query on a many-to-many relation', function () {
424+
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 \'*\'';
425+
runQuery({'tags.name': {$regex: /wor%ld/}})
426+
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
427+
});
428+
429+
it('should be able to perform a negated match query on a many-to-many relation', function () {
430+
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 \'*\'';
431+
runQuery({'tags.name': {$not: /wor%ld/}})
432+
.should.eql(`select * from \`posts\` where \`posts\`.\`id\` in (${innerQuery})`);
433+
});
410434
});
411435

412436
describe('RegExp/Like queries', function () {

0 commit comments

Comments
 (0)