rsql-mongodb
Version:
Converting RSQL queries to MongoDB queries
375 lines (314 loc) • 10.3 kB
JavaScript
const { ObjectId } = require('bson');
function setType(input) {
var typedInput = input;
var matchQuotes = /^(["']{1})(.*)(["']{1})$/g;
var matchQuotesResults = matchQuotes.exec(input);
var matchDate = /(\d{4})-(\d{2})-(\d{2})/g;
if(matchQuotesResults){
typedInput = matchQuotesResults[2];
}
else if(input === 'true'){
typedInput = true;
}
else if(input === 'false'){
typedInput = false;
}
else if(input === 'null'){
typedInput = null;
}
else if (!isNaN(Number(input))) {
typedInput = Number(input);
}
else if(matchDate.exec(input)){
if(Date.parse(input)){
var isoUTCDate = new Date(input).toISOString();
typedInput = new Date(isoUTCDate);
}
else{
throw "Invalid Date."
}
}
return typedInput;
}
function setTypeObjectId(input) {
// Remove quotes and double quotes
formatedInput = input.replace(/['"]+/g, '');
if(ObjectId.isValid(formatedInput))
return new ObjectId(formatedInput);
else
return setType(input);
}
module.exports = function (input) {
// Define variables
var outputString = "";
var outputTab = [];
var logicalsTab = [];
var specialOperator = false;
// Define logical & special operators
var logicals = [';', ','];
var specialOperators = ['=in=', '=out='];
var expForId = ["_id"];
var isInQuotes = false;
// Apply Shunting-yard algorithm applied to this use case
//
// Loop for each character of the input string
for(var i = 0; i < input.length; i++) {
// Move into input string
var character = input[i];
// Check if the character is a single quote and toggle the isInQuotes flag
if (character === "'") {
isInQuotes = !isInQuotes;
}
// If the character is a logical operator
if(!isInQuotes && logicals.indexOf(character) !== -1) {
// If the character is a special operator
if(specialOperator){
outputString += character;
}
// Manage escape character
else if(outputString[outputString.length - 1] == "\\"){
outputString = outputString.substring(0, outputString.length - 1);
outputString += character;
}
else{
// Get last logical operator in the 'logicalsTab'
var lastLogical = logicalsTab[logicalsTab.length - 1];
// If there is something into buffer 'outputString' push it into 'outputTab'
if(outputString){
outputTab.push(outputString);
outputString = "";
}
// Push the logical character into 'outputTab' if the last logical operator is not the same that the current
while(logicals.indexOf(lastLogical) !== -1) {
if(lastLogical == character){
logicalsTab.pop();
}
else{
outputTab.push(logicalsTab.pop());
}
lastLogical = logicalsTab[logicalsTab.length - 1];
}
// Push the character into 'logicalsTab'
logicalsTab.push(character);
}
}
// If the character is an open parenthesis
else if(character === "(") {
// if the parenthesis is value of a special operator then push it into 'outputString' buffer
if(specialOperators.indexOf(outputString.substring(outputString.length - 4, outputString.length)) !== -1 || specialOperators.indexOf(outputString.substring(outputString.length - 5, outputString.length)) !== -1){
specialOperator = true;
outputString += character;
}
// Manage escape character
else if(outputString[outputString.length - 1] == "\\"){
outputString = outputString.substring(0, outputString.length - 1);
outputString += character;
}
// Else push the character into the 'logicalsTab'
else{
// Push all operator presents in 'logicalsTab' into 'outputTab'
while(logicalsTab.length > 0) {
outputTab.push(logicalsTab.pop());
}
logicalsTab.push(character);
outputTab.push(character);
}
}
// If the character is a closed parenthesis
else if(character === ")") {
// if the parenthesis is value of a special operator then push it into 'outputString' buffer
if(specialOperator){
if(outputString[outputString.length - 1] == "\\"){
outputString = outputString.substring(0, outputString.length - 1);
outputString += character;
}
else{
specialOperator = false;
outputString += character;
}
}
// Manage escape character
else if(outputString[outputString.length - 1] == "\\"){
outputString = outputString.substring(0, outputString.length - 1);
outputString += character;
}
else{
// If there is something into buffer 'outputString' push it into 'outputTab'
if(outputString){
outputTab.push(outputString);
outputString = "";
}
// Push all operator presents in the parenthesis into 'outputTab'
while(logicalsTab.length > 0 && logicalsTab[logicalsTab.length - 1] !== "(") {
outputTab.push(logicalsTab.pop());
}
// Remove the open parenthesis from 'logicalsTab'
logicalsTab.pop();
outputTab.push(character);
}
}
// If the character is not an operator push it into the 'outputString' buffer
else{
outputString += character;
}
}
// If there is something into buffer 'outputString' push it into 'outputTab'
if(outputString){
outputTab.push(outputString);
outputString = "";
}
// Push all operator presents in 'logicalsTab' into 'outputTab'
while(logicalsTab.length > 0) {
outputTab.push(logicalsTab.pop());
}
// Now format the MongoDb Query
// Define variables
var mongoStack = [];
var mongoQuery = [];
var tmpPrecedence = [];
var lastLogical = "";
var lastLogicalBeforePrecedence = ""
for(var i = 0; i < outputTab.length; i++) {
if(logicals.indexOf(outputTab[i]) !== -1){
var newValue = {};
var tmpArray = [];
switch(outputTab[i]){
case ";":
case ",":
if(i == (outputTab.length -1) || (mongoQuery.length == 1)){
while(mongoQuery.length > 0) {
tmpArray.push(mongoQuery.shift())
}
}
while(mongoStack.length > 0) {
tmpArray.push(mongoStack.shift())
}
if(outputTab[i] == ";"){
lastLogical = '$and';
newValue[lastLogical] = tmpArray;
}
else{
lastLogical = '$or';
newValue[lastLogical] = tmpArray;
}
break;
default:
throw "Logical operator not supported."
}
mongoQuery.push(newValue);
}
else if( outputTab[i] == '('){
tmpPrecedence = mongoQuery.shift();
lastLogicalBeforePrecedence = lastLogical;
}
else if( outputTab[i] == ')'){
if(tmpPrecedence){
tmpPrecedence[lastLogicalBeforePrecedence].push(mongoQuery.shift());
mongoQuery.push(tmpPrecedence);
}else{
}
}
else{
// Verify if the is no injections
var mongoQueryOperators = /(\$\w+:)/g;
var badQuery = mongoQueryOperators.exec(outputTab[i]);
if(badQuery){
throw "Injection detected."
}
// Split the query
var rsqlOperators = /(.*)(==|!=|=gt=|=ge=|=lt=|=le=|=in=|=out=|=regex=|=notregex=|=exists=)(.*)/g;
var rsqlQuery = rsqlOperators.exec(outputTab[i]);
try {
var exp1 = rsqlQuery[1];
var exp2 = rsqlQuery[3];
var operator = rsqlQuery[2];
}
catch(e){
throw "Wrong RSQL query. No operator found."
}
try{
var typedExp2 = setType(exp2);
if(expForId.indexOf(exp1) !== -1)
typedExp2 = setTypeObjectId(exp2);
var mongoOperatorQuery = {};
switch(operator){
case "==":
mongoOperatorQuery[exp1] = typedExp2;
break;
case "!=":
mongoOperatorQuery[exp1] = { $ne: typedExp2 };
break;
case "=gt=":
mongoOperatorQuery[exp1] = { $gt: typedExp2 };
break;
case "=ge=":
mongoOperatorQuery[exp1] = { $gte: typedExp2 };
break;
case "=lt=":
mongoOperatorQuery[exp1] = { $lt: typedExp2 };
break;
case "=le=":
mongoOperatorQuery[exp1] = { $lte: typedExp2 };
break;
case "=in=":
if(typedExp2[0] == "(")
typedExp2 = typedExp2.slice(1);
if(typedExp2[typedExp2.length -1] == ")")
typedExp2 = typedExp2.slice(0, typedExp2.length -1);
var typedValues = new Array();
for ( var token of typedExp2.split(",") ) {
var value = setType(token.trim());
if(expForId.indexOf(exp1) !== -1)
value = setTypeObjectId(value);
typedValues.push(value);
}
mongoOperatorQuery[exp1] = { $in: typedValues };
break;
case "=out=":
if(typedExp2[0] == "(")
typedExp2 = typedExp2.slice(1);
if(typedExp2[typedExp2.length -1] == ")")
typedExp2 = typedExp2.slice(0, typedExp2.length -1);
var typedValues = new Array();
for ( var token of typedExp2.split(",") ) {
var value = setType(token.trim());
if(expForId.indexOf(exp1) !== -1)
value = setTypeObjectId(value);
typedValues.push(value);
}
mongoOperatorQuery[exp1] = { $nin: typedValues };
break;
case "=regex=":
{
var expArr = exp2.split(/(=)(?=(?:[^"]|"[^"]*")*$)/g);
const regex = new RegExp(expArr[0]);
regex.test('');
mongoOperatorQuery[exp1] = { $regex: `${setType(expArr[0])}`, $options: expArr[2] || "" };
}
break;
case "=notregex=":
{
var expArr = exp2.split(/(=)(?=(?:[^"]|"[^"]*")*$)/g);
const regex = new RegExp(expArr[0]);
regex.test('');
mongoOperatorQuery[exp1] = { $not: { $regex: `${setType(expArr[0])}`, $options: expArr[2] || "" } };
}
break;
case "=exists=":
mongoOperatorQuery[exp1] = { $exists: typedExp2 };
break;
default:
throw "Operator not supported."
}
}
catch(error){
throw error;
}
mongoStack.push(mongoOperatorQuery);
}
}
if(mongoStack.length == 1 && mongoQuery.length == 0){
mongoQuery = mongoStack;
}
return mongoQuery[0] || null;
}