From cc25cfc1b988d8120fefc9743fefb18ab88871d7 Mon Sep 17 00:00:00 2001 From: Vikas Potluri Date: Sun, 18 Aug 2024 17:51:30 +0000 Subject: [PATCH] :bug: added match support to nested relations --- packages/mongo-knex/lib/convertor.js | 55 ++++++++++++------- .../mongo-knex/test/unit/convertor.test.js | 24 ++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/packages/mongo-knex/lib/convertor.js b/packages/mongo-knex/lib/convertor.js index 07ccfaf1..d0f8fda4 100644 --- a/packages/mongo-knex/lib/convertor.js +++ b/packages/mongo-knex/lib/convertor.js @@ -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 @@ -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; @@ -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}`); @@ -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 { @@ -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; @@ -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 () { @@ -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) { @@ -469,20 +481,8 @@ 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; } @@ -490,6 +490,23 @@ class MongoToKnex { 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'} */ diff --git a/packages/mongo-knex/test/unit/convertor.test.js b/packages/mongo-knex/test/unit/convertor.test.js index 6c3d4d94..3db9af87 100644 --- a/packages/mongo-knex/test/unit/convertor.test.js +++ b/packages/mongo-knex/test/unit/convertor.test.js @@ -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 () {