Skip to content

Commit

Permalink
Added support for $regex & $not ops to mongo-knex
Browse files Browse the repository at this point in the history
ref: #22

- NQL lang will parse ~ ~^ and ~$ into $regex and $not
- This adds support in the convertor for $regex and $not in these simple cases
- As we are moving into SQL, we have to de-escape the regex and convert it back into a string but regexp.source makes this fairly easy
- Had to add regexEnabledJSONStringify as a side effect because JSON.stringify doesn't expect or handle regex
  • Loading branch information
ErisDS committed Mar 2, 2022
1 parent 7019c2c commit 7b8798a
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 8 deletions.
64 changes: 56 additions & 8 deletions packages/mongo-knex/lib/convertor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const compOps = {
$lt: '<',
$lte: '<=',
$in: 'in',
$nin: 'not in'
$nin: 'not in',
$regex: 'like',
$not: 'not like'
};

const isOp = key => key.charAt(0) === '$';
Expand All @@ -24,6 +26,39 @@ 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);

/**
* JSON Stringify with RegExp support
* @param {Object} json
* @returns
*/
const stringify = (json) => {
return JSON.stringify(json, function (key, value) {
if (value instanceof RegExp) {
return value.toString();
}
return value;
});
};

const processRegExp = ({source, ignoreCase}) => {
source = source.replace(/\\([.*+?^${}()|[\]\\])/g, '$1');

if (ignoreCase) {
source = source.toLowerCase();
}

// For starts with and ends with in SQL we have to put the wildcard at the opposite end of the string to the regex symbol!
if (source.startsWith('^')) {
source = source.replace('^', '') + '%';
} else if (source.endsWith('$')) {
source = '%' + source.replace('$', '');
} else {
source = '%' + source + '%';
}

return {source, ignoreCase};
};

class MongoToKnex {
/**
*
Expand Down Expand Up @@ -199,13 +234,13 @@ class MongoToKnex {
debug(`(buildRelationQuery)`);

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

const groupedRelations = this.groupRelationStatements(relations, mode);

if (debugExtended.enabled) {
debugExtended(`(buildRelationQuery) grouped: ${JSON.stringify(groupedRelations)}`);
debugExtended(`(buildRelationQuery) grouped: ${stringify(groupedRelations)}`);
}

// CASE: {tags: [where clause, where clause], tags_123: [where clause], authors: [where clause, where clause]}
Expand Down Expand Up @@ -389,8 +424,8 @@ class MongoToKnex {
*/
buildComparison(qb, mode, statement, op, value, group) {
const comp = compOps[op] || '=';
const whereType = this.processWhereType(mode, op, value);
const processedStatement = this.processStatement(statement, op, value);
let whereType = this.processWhereType(mode, op, value);

debug(`(buildComparison) mode: ${mode}, op: ${op}, isRelation: ${processedStatement.isRelation}, group: ${group}`);

Expand Down Expand Up @@ -418,6 +453,19 @@ 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} ?`, [column, value]);
return;
}
}

debug(`(buildComparison) whereType: ${whereType}, statement: ${statement}, op: ${op}, comp: ${comp}, value: ${value}`);
qb[whereType](column, comp, value);
}
Expand All @@ -429,7 +477,7 @@ class MongoToKnex {
debug(`(buildWhereClause) mode: ${mode}, statement: ${statement}`);

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

// CASE sub is an atomic value, we use "eq" as default operator
Expand Down Expand Up @@ -458,7 +506,7 @@ class MongoToKnex {
debug(`(buildWhereGroup) mode: ${mode}, whereType: ${whereType}`);

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

qb[whereType]((_qb) => {
Expand All @@ -480,7 +528,7 @@ class MongoToKnex {
debug(`(buildQuery) mode: ${mode}`);

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

_.forIn(sub, (value, key) => {
Expand All @@ -505,7 +553,7 @@ class MongoToKnex {

// DEBUG=mongo-knex:converter,mongo-knex:converter-extended
if (debugExtended.enabled) {
debugExtended(`(processJSON) ${JSON.stringify(mongoJSON)}`);
debugExtended(`(processJSON) ${stringify(mongoJSON)}`);
}

// 'and' is the default behaviour
Expand Down
15 changes: 15 additions & 0 deletions packages/mongo-knex/test/unit/convertor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ describe('Comparison Query Operators', function () {
runQuery({tags: {$nin: ['video', 'audio']}})
.should.eql('select * from `posts` where `posts`.`tags` not in (\'video\', \'audio\')');
});

it('can match like', function () {
runQuery({email: {$regex: /Gmail\.com/i}})
.should.eql('select * from `posts` where lower(`posts`.`email`) like \'%gmail.com%\'');
});

it('can match like with startswith', function () {
runQuery({email: {$regex: /^Gmail\.com/i}})
.should.eql('select * from `posts` where lower(`posts`.`email`) like \'gmail.com%\'');
});

it('can match like with endswith', function () {
runQuery({email: {$regex: /Gmail\.com$/i}})
.should.eql('select * from `posts` where lower(`posts`.`email`) like \'%gmail.com\'');
});
});

describe('Logical Query Operators', function () {
Expand Down

0 comments on commit 7b8798a

Please sign in to comment.