Эх сурвалжийг харах

HUE-4985 [editor] Query builder fix in new editor

Enrico Berti 9 жил өмнө
parent
commit
39aadcf6c7

+ 494 - 528
desktop/core/src/desktop/static/desktop/js/queryBuilder.js

@@ -15,538 +15,504 @@
 // limitations under the License.
 // limitations under the License.
 
 
 (function (window) {
 (function (window) {
-    'use strict';
-
-    function defineQueryBuilder() {
-        var QueryBuilder = {};
-        const PROJECT = 'Project',
-              AGGREGATE = 'Aggregate',
-              ORDER = 'Order',
-              FROM = 'From',
-              FILTER = 'Filter',
-              SELECT = "Select",
-              SELECT_DISTINCT = "Select distinct",
-              COUNT = "Count",
-              COUNT_DISTINCT = "Count distinct",
-              SUM = "Sum",
-              MINIMUM = "Minimum",
-              MAXIMUM = "Maximum",
-              AVERAGE = "Average",
-              IS_NULL = "Is null",
-              IS_NOT_NULL = "Is not null",
-              EQUAL_TO = "Equal to",
-              NOT_EQUAL_TO = "Not equal to",
-              GREATER_THAN = "Greater than",
-              LESS_THAN = "Less than",
-              ASCENDING = "Ascending",
-              DESCENDING = "Descending";
-
-        /** Callback to be used on rule add. */
-        QueryBuilder.callbackOnRuleAdd = null;
-
-        /**
-         * Format string
-         * @param {string} Input template string
-         * @param {...Object} Objects to be used in template
-         */
-        QueryBuilder.formatString = function (template) {
-            var i, regEx;
-            for (i = 1; i < arguments.length; i += 1) {
-                regEx = new RegExp("\\{" + (i - 1) + "\\}", "gm");
-                template = template.replace(regEx, arguments[i]);
-            }
-            return template;
-        };
-
-        /**
-         * Get row by datbase, table, column, and rule
-         * @param {string} database
-         * @param {string} table
-         * @param {string} column
-         * @param {string} rule
-         * @return {Object} Query rule row
-         */
-        QueryBuilder.getRule = function (database, table, column, rule) {
-            return $(".queryBuilderRow[database='${database}'][table='${table}'][column='${column}'][rule='${rule}']");
-        };
-
-        /**
-         * Count number of rules by database, table, column and category combination.
-         * @param {string} database
-         * @param {string} table
-         * @param {string} column
-         * @param {string} category
-         * @return {number} Number of matching rows
-         */
-        QueryBuilder.countRulesByCategory = function(database, table, column, category) {
-            return $(".queryBuilderRow[database='${database}'][table='${table}'][column='${column}'][category='${category}']").length;
-        };
-
-        /**
-         * Get the first row matching the database, table and column.
-         * @param {string} database
-         * @param {string} table
-         * @param {string} column
-         * @return {Object} Query row
-         */
-        QueryBuilder.getFirstMatchingRow = function(database, table, column) {
-            return $(".queryBuilderRow[database='${database}'][table='${table}'][column='${column}']").first();
-        };
-
-        /**
-         * Count the number of rules matching database, table and column.
-         * @param {string} database
-         * @param {string} table
-         * @param {string} column
-         * @return {number} Number of matching rules
-         */
-        QueryBuilder.countMatchingRows = function(database, table, column) {
-            return $(".queryBuilderRow[database='${database}'][table='${table}'][column='${column}']").length;
-        };
 
 
-        /**
-         * Deletes a rule from the query builder.
-         * @param {Object} row Row to delete
-         */
-        QueryBuilder.deleteRule = function(row) {
-            var database = row.attr('database');
-            var table = row.attr('table');
-            var column = row.attr('column');
-            var isVisible = row.find('.databaseAndTable').css('visibility');
-
-            // Remove the row
-            row.remove();
-
-            // Check if row was the first row, and if there are other rows for this column
-            if (isVisible == 'visible' && QueryBuilder.countMatchingRows(database, table, column) > 0) {
-                // Change the visibility for the next row
-                var nextRow = QueryBuilder.getFirstMatchingRow(database, table, column);
-                nextRow.children('.databaseAndTable').css('visibility', 'visible');
-                nextRow.children('.column').css('visibility', 'visible');
-            }
+  function defineQueryBuilder() {
+    var QueryBuilder = {};
+    var PROJECT = "Project",
+      AGGREGATE = "Aggregate",
+      ORDER = "Order",
+      FROM = "From",
+      FILTER = "Filter",
+      SELECT = "Select",
+      SELECT_DISTINCT = "Select distinct",
+      COUNT = "Count",
+      COUNT_DISTINCT = "Count distinct",
+      SUM = "Sum",
+      MINIMUM = "Minimum",
+      MAXIMUM = "Maximum",
+      AVERAGE = "Average",
+      IS_NULL = "Is null",
+      IS_NOT_NULL = "Is not null",
+      EQUAL_TO = "Equal to",
+      NOT_EQUAL_TO = "Not equal to",
+      GREATER_THAN = "Greater than",
+      LESS_THAN = "Less than",
+      ASCENDING = "Ascending",
+      DESCENDING = "Descending";
+
+    /** Callback to be used on rule add. */
+    QueryBuilder.callbackOnRuleAdd = null;
+
+    /**
+     * Format string
+     * @param {string} Input template string
+     * @param {...Object} Objects to be used in template
+     */
+    QueryBuilder.formatString = function (template) {
+      var i, regEx;
+      for (i = 1; i < arguments.length; i += 1) {
+        regEx = new RegExp("\\{" + (i - 1) + "\\}", "gm");
+        template = template.replace(regEx, arguments[i]);
+      }
+      return template;
+    };
+
+    /**
+     * Get row by datbase, table, column, and rule
+     * @param {string} database
+     * @param {string} table
+     * @param {string} column
+     * @param {string} rule
+     * @return {Object} Query rule row
+     */
+    QueryBuilder.getRule = function (database, table, column, rule) {
+      return $('.queryBuilderRow[database=\'' + database + '\'][table=\'' + table + '\'][column=\'' + column + '\'][rule=\'' + rule + '\']');
+    };
+
+    /**
+     * Count number of rules by database, table, column and category combination.
+     * @param {string} database
+     * @param {string} table
+     * @param {string} column
+     * @param {string} category
+     * @return {number} Number of matching rows
+     */
+    QueryBuilder.countRulesByCategory = function (database, table, column, category) {
+      return $('.queryBuilderRow[database=\'' + database + '\'][table=\'' + table + '\'][column=\'' + column + '\'][category=\'' + category + '\']').length;
+    };
+
+    /**
+     * Get the first row matching the database, table and column.
+     * @param {string} database
+     * @param {string} table
+     * @param {string} column
+     * @return {Object} Query row
+     */
+    QueryBuilder.getFirstMatchingRow = function (database, table, column) {
+      return $('.queryBuilderRow[database=\'' + database + '\'][table=\'' + table + '\'][column=\'' + column + '\']').first();
+    };
+
+    /**
+     * Count the number of rules matching database, table and column.
+     * @param {string} database
+     * @param {string} table
+     * @param {string} column
+     * @return {number} Number of matching rules
+     */
+    QueryBuilder.countMatchingRows = function (database, table, column) {
+      return $('.queryBuilderRow[database=\'' + database + '\'][table=\'' + table + '\'][column=\'' + column + '\']').length;
+    };
+
+    /**
+     * Deletes a rule from the query builder.
+     * @param {Object} row Row to delete
+     */
+    QueryBuilder.deleteRule = function (row) {
+      var database = row.attr('database');
+      var table = row.attr('table');
+      var column = row.attr('column');
+      var isVisible = row.find('.databaseAndTable').css('visibility');
+
+      // Remove the row
+      row.remove();
+
+      // Check if row was the first row, and if there are other rows for this column
+      if (isVisible == 'visible' && QueryBuilder.countMatchingRows(database, table, column) > 0) {
+        // Change the visibility for the next row
+        var nextRow = QueryBuilder.getFirstMatchingRow(database, table, column);
+        nextRow.children('.databaseAndTable').css('visibility', 'visible');
+        nextRow.children('.column').css('visibility', 'visible');
+      }
+    };
+
+    /**
+     * Add rule to table.
+     * @param{string} database Database to use
+     * @param{string} table Table to use
+     * @param{string} column Column to use
+     * @param{string} rule Rule name
+     * @param{string} category Category for the rule
+     * @param{string} template Template string to use for building the query
+     * @param{boolean} hasInput Set to true if the rule uses input
+     * @param{boolean} allowDuplicate Set to true if the rule allows duplicates
+     */
+    QueryBuilder.addRule = function (database, table, column, rule, category, template, hasInput, allowDuplicate) {
+      // Check if we allow duplicates of this rule
+      if (!allowDuplicate) {
+        // Check if rule already exists
+        if (QueryBuilder.getRule(database, table, column, rule).length) {
+          // Don't add rule
+          return;
+        }
+      }
+
+      // Count matching rules, used later to hide
+      var numOfMatchingRows = QueryBuilder.countMatchingRows(database, table, column);
+
+      // Should we hide databaseAndTable and column?
+      var visibility = '';
+      if (numOfMatchingRows) {
+        visibility = 'style=\'visibility:hidden\'';
+      }
+
+      // Should we have an input field?
+      var inputField = '';
+      if (hasInput) {
+        inputField = '<input type=\'text\' placeholder=\'Add here\' required>';
+      }
+
+      // Used to store new rule
+      var row = '<tr class=\'queryBuilderRow\' database=\'' + database + '\' table=\'' + table + '\' column=\'' + column + '\' rule=\'' + rule + '\' category=\'' + category + '\' template=\'' + template + '\'>\n                       <td class=\'databaseAndTable\' ' + visibility + '>' + database + '.' + table + '</td>\n                       <td class=\'column\' ' + visibility + '>' + column + '</td>\n                       <td class=\'rule\'>' + rule + '</td>\n                       <td class=\'input\'>' + inputField + '</td>\n                       <td class=\'deleteRule\' onclick=\'QueryBuilder.deleteRule($(this).closest("tr"))\'><i class=\'fa fa-times\'></i></td>\n                       </tr>';
+
+      // Check if the column is already in the rules
+      if (numOfMatchingRows) {
+        // Add after the last rule for this column
+        $('.queryBuilderRow[database=\'' + database + '\'][table=\'' + table + '\'][column=\'' + column + '\']').last().after(row);
+      } else {
+        // Add at end of rules
+        $('#queryBuilder').append(row);
+      }
+
+      // Perform callback.
+      QueryBuilder.callbackOnRuleAdd();
+    };
+
+    /**
+     * Add database and table to query from list.
+     * @param {string} category
+     * @param {object} query JSON query object
+     * @param {object} row Row rule object
+     */
+    QueryBuilder.addRuleToQuery = function (category, query, row) {
+      query[category].push({
+        "rule": row.attr('rule'),
+        "column": row.attr('column'),
+        "table": row.attr('table'),
+        "input": row.find('input').val(),
+        "template": row.attr('template')
+      });
+
+      // Add database and table to data sources
+      if (!query[FROM].hasOwnProperty(row.attr('database'))) {
+        query[FROM][row.attr('database')] = [];
+      }
+      if (!query[FROM][row.attr('database')].hasOwnProperty(row.attr('table'))) {
+        query[FROM][row.attr('database')].push(row.attr('table'));
+      }
+    };
+
+    /**
+     * Convert the rules into a JSON object.
+     * @return {Object} JSON representation of the query
+     */
+    QueryBuilder.convertRulesToJson = function () {
+      var _query;
+
+      // Stores the JSONified query
+      var query = (_query = {}, _query[PROJECT] = [], _query[AGGREGATE] = [], _query[FILTER] = [], _query[ORDER] = [], _query[FROM] = {}, _query);
+
+      // Populate project
+      $('.queryBuilderRow[category=\'' + PROJECT + '\']').each(function () {
+        QueryBuilder.addRuleToQuery(PROJECT, query, $(this));
+      });
+
+      // Populate aggregate
+      $('.queryBuilderRow[category=\'' + AGGREGATE + '\']').each(function () {
+        QueryBuilder.addRuleToQuery(AGGREGATE, query, $(this));
+      });
+
+      // Populate filter
+      $('.queryBuilderRow[category=\'' + FILTER + '\']').each(function () {
+        QueryBuilder.addRuleToQuery(FILTER, query, $(this));
+      });
+
+      // Populate order
+      $('.queryBuilderRow[category=\'' + ORDER + '\']').each(function () {
+        QueryBuilder.addRuleToQuery(ORDER, query, $(this));
+      });
+
+      return query;
+    };
+
+    // Build a SQL query based on rules
+    /**
+     * Build a HiveQL query based on the provided rules.
+     * @param {string} X
+     * @return {{status: String, message: String, query: String}} JSON
+     *         object result. A 'status' of 'fail' will have error message in
+     *         'message'. A 'status' of 'pass' will have query in 'query'.
+     */
+    QueryBuilder.buildHiveQuery = function () {
+      // Get the rules in JSON format
+      var rules = QueryBuilder.convertRulesToJson();
+
+      // First, validate provided rules
+      if (rules[PROJECT].length < 1 && rules[AGGREGATE].length < 1) {
+        return {
+          "status": "fail",
+          "message": "Query requires a select or aggregate."
         };
         };
-
-        /**
-         * Add rule to table.
-         * @param{string} database Database to use
-         * @param{string} table Table to use
-         * @param{string} column Column to use
-         * @param{string} rule Rule name
-         * @param{string} category Category for the rule
-         * @param{string} template Template string to use for building the query
-         * @param{boolean} hasInput Set to true if the rule uses input
-         * @param{boolean} allowDuplicate Set to true if the rule allows duplicates
-         */
-        QueryBuilder.addRule = function(database, table, column, rule, category, template, hasInput, allowDuplicate) {
-            // Check if we allow duplicates of this rule
-            if (!allowDuplicate) {
-                // Check if rule already exists
-                if (QueryBuilder.getRule(database, table, column, rule).length) {
-                    // Don't add rule
-                    return;
+      }
+
+      // Stores the final rules
+      var finalQuery;
+      // Indent for select
+      var selectIndent;
+      // Check if using SELECT DISTINCT
+      if ($('.queryBuilderRow[rule=\'' + SELECT_DISTINCT + '\']').length > 0) {
+        finalQuery = 'SELECT DISTINCT ';
+        selectIndent = '                ';
+      } else {
+        finalQuery = 'SELECT ';
+        selectIndent = '       ';
+      }
+
+      // Build SELECT rules
+      var selectColumns = [];
+      $.each(rules[PROJECT], function (index, value) {
+        selectColumns.push(value.table + '.' + value.column);
+      });
+
+      // Build aggregate rules using templates
+      // {0} == database
+      // {1} == table
+      // {2} == input
+      var aggregateColumns = [];
+      $.each(rules[AGGREGATE], function (index, value) {
+        aggregateColumns.push(QueryBuilder.formatString(value.template, value.table, value.column, value.input));
+      });
+
+      // Add select and aggregate columns to final rules
+      finalQuery += $.merge($.merge([], selectColumns), aggregateColumns).join(',\n' + selectIndent);
+
+      // Build FROM rules
+      var databaseAndTables = [];
+      $.each(rules[FROM], function (database, tableArray) {
+        $.each(tableArray, function (i, table) {
+          databaseAndTables.push(database + '.' + table);
+        });
+      });
+      $.unique(databaseAndTables);
+      finalQuery += '\n' + 'FROM ' + databaseAndTables.join(',\n     ');
+
+      // Build WHERE rules
+      var whereColumns = [];
+      $.each(rules[FILTER], function (index, value) {
+        // Check if the string is numeric
+        if (/^\d+$/.test(value.input)) {
+          whereColumns.push(QueryBuilder.formatString(value.template, value.table, value.column, value.input));
+        } else {
+          whereColumns.push(QueryBuilder.formatString(value.template, value.table, value.column, "'" + value.input + "'"));
+        }
+      });
+      if (whereColumns.length > 0) {
+        finalQuery += '\n' + 'WHERE ' + whereColumns.join('\n  AND ');
+      }
+
+      // Build GROUP rules
+      // If we have any aggregates, group on all projected columns
+      if (aggregateColumns.length > 0) {
+        if (selectColumns.length > 0) {
+          finalQuery += '\n' + 'GROUP BY ' + selectColumns.join(',\n         ');
+        }
+      }
+
+      // Build ORDER rules
+      var orderColumns = [];
+      $.each(rules[ORDER], function (index, value) {
+        orderColumns.push(QueryBuilder.formatString(value.template, value.table, value.column));
+      });
+      if (orderColumns.length > 0) {
+        finalQuery += '\n' + 'ORDER BY ' + orderColumns.join(',\n         ');
+      }
+
+      // Add final semicolon
+      finalQuery += ';';
+
+      // Return the output query
+      return {
+        "status": "pass",
+        "query": finalQuery
+      };
+    };
+
+    /**
+     * Binds context menu to a provided selector class.
+     * Each object using the selector class is expected to have several attributes: 'database', 'table' and 'column'
+     * Each rule internally has a template that will be used. The format is '{0}' -> database, {1} -> table, {2} -> input
+     * @param {string} selectorClass The selector class to use.
+     */
+    QueryBuilder.bindMenu = function (selectorClass, callbackOnRuleAdd) {
+      QueryBuilder.callbackOnRuleAdd = callbackOnRuleAdd;
+      $.contextMenu({
+        selector: selectorClass,
+        trigger: 'right',
+        zIndex: 1000,
+        build: function build($trigger, e) {
+          var _items, _items2, _items3, _items4, _items5;
+
+          // Get the database, table, and column
+          var database = $trigger.attr('database');
+          var table = $trigger.attr('table');
+          var column = $trigger.attr('column');
+          var result = {
+            items: (_items5 = {
+              "column-name": {
+                name: column,
+                disabled: true,
+                className: 'muted'
+              },
+              "sep1": "---------"
+            }, _items5[PROJECT] = {
+              "name": PROJECT,
+              "items": (_items = {}, _items[SELECT] = {
+                "name": SELECT,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, SELECT, PROJECT, "{0}", false, false);
                 }
                 }
-            }
-
-            // Count matching rules, used later to hide
-            var numOfMatchingRows = QueryBuilder.countMatchingRows(database, table, column);
-
-            // Should we hide databaseAndTable and column?
-            var visibility = '';
-            if (numOfMatchingRows) {
-                visibility = "style='visibility:hidden'";
-            }
-
-            // Should we have an input field?
-            var inputField = '';
-            if (hasInput) {
-                inputField = "<input type='text' placeholder='Add here' required>";
-            }
-
-            // Used to store new rule
-            var row = "<tr class='queryBuilderRow' database='${database}' table='${table}' column='${column}' rule='${rule}' category='${category}' template='${template}'>" +
-                       "<td class='databaseAndTable' ${visibility}>${database}.${table}</td>" +
-                       "<td class='column' ${visibility}>${column}</td>" +
-                       "<td class='rule'>${rule}</td>" +
-                       "<td class='input'>${inputField}</td>" +
-                       "<td class='deleteRule' onclick='QueryBuilder.deleteRule($(this).closest('tr'))'><i class='fa fa-times'></i></td>" +
-                       "</tr>";
-
-            // Check if the column is already in the rules
-            if (numOfMatchingRows) {
-                // Add after the last rule for this column
-                $(".queryBuilderRow[database='${database}'][table='${table}'][column='${column}']").last().after(row);
-            } else {
-                // Add at end of rules
-                $('#queryBuilder').append(row);
-            }
-
-            // Perform callback.
-            QueryBuilder.callbackOnRuleAdd();
-        };
-
-
-        /**
-         * Add database and table to query from list.
-         * @param {string} category
-         * @param {object} query JSON query object
-         * @param {object} row Row rule object
-         */
-        QueryBuilder.addRuleToQuery = function(category, query, row) {
-            query[category].push({
-                "rule": row.attr('rule'),
-                "column": row.attr('column'),
-                "table": row.attr('table'),
-                "input": row.find('input').val(),
-                "template": row.attr('template')
-            });
-
-            // Add database and table to data sources
-            if (!query[FROM].hasOwnProperty(row.attr('database'))) {
-                query[FROM][row.attr('database')] = [];
-            }
-            if (!query[FROM][row.attr('database')].hasOwnProperty(row.attr('table'))) {
-                query[FROM][row.attr('database')].push(row.attr('table'));
-            }
-        };
-
-        /**
-         * Convert the rules into a JSON object.
-         * @return {Object} JSON representation of the query
-         */
-        QueryBuilder.convertRulesToJson = function() {
-            // Stores the JSONified query
-            var query = {
-                [PROJECT]: [],
-                [AGGREGATE]: [],
-                [FILTER]: [],
-                [ORDER]: [],
-                [FROM]: {}
-            };
-
-            // Populate project
-            $(".queryBuilderRow[category='${PROJECT}']").each(function() {
-                QueryBuilder.addRuleToQuery(PROJECT, query, $(this));
-            });
-
-            // Populate aggregate
-            $(".queryBuilderRow[category='${AGGREGATE}']").each(function() {
-                QueryBuilder.addRuleToQuery(AGGREGATE, query, $(this));
-            });
-
-            // Populate filter
-            $(".queryBuilderRow[category='${FILTER}']").each(function() {
-                QueryBuilder.addRuleToQuery(FILTER, query, $(this));
-            });
-
-            // Populate order
-            $(".queryBuilderRow[category='${ORDER}']").each(function() {
-                QueryBuilder.addRuleToQuery(ORDER, query, $(this));
-            });
-
-            return query;
-        };
-
-        // Build a SQL query based on rules
-        /**
-         * Build a HiveQL query based on the provided rules.
-         * @param {string} X
-         * @return {{status: String, message: String, query: String}} JSON
-         *         object result. A 'status' of 'fail' will have error message in
-         *         'message'. A 'status' of 'pass' will have query in 'query'.
-         */
-        QueryBuilder.buildHiveQuery = function() {
-            // Get the rules in JSON format
-            var rules = QueryBuilder.convertRulesToJson();
-
-            // First, validate provided rules
-            if (rules[PROJECT].length < 1 && rules[AGGREGATE].length < 1) {
-                return {
-                    "status": "fail",
-                    "message": "Query requires a select or aggregate."
-                };
-            }
-
-            // Stores the final rules
-            var finalQuery;
-            // Indent for select
-            var selectIndent;
-            // Check if using SELECT DISTINCT
-            if ($(".queryBuilderRow[rule='${SELECT_DISTINCT}']").length > 0) {
-                finalQuery = 'SELECT DISTINCT ';
-                selectIndent = '                ';
-            } else {
-                finalQuery = 'SELECT ';
-                selectIndent = '       ';
-            }
-
-            // Build SELECT rules
-            var selectColumns = [];
-            $.each(rules[PROJECT], function(index, value) {
-                selectColumns.push(value.table + '.' + value.column);
-            });
-
-            // Build aggregate rules using templates
-            // {0} == database
-            // {1} == table
-            // {2} == input
-            var aggregateColumns = [];
-            $.each(rules[AGGREGATE], function(index, value) {
-                aggregateColumns.push(QueryBuilder.formatString(value.template, value.table, value.column, value.input));
-            });
-
-            // Add select and aggregate columns to final rules
-            finalQuery += $.merge($.merge([], selectColumns), aggregateColumns).join(',\n' + selectIndent);
-
-            // Build FROM rules
-            var databaseAndTables = [];
-            $.each(rules[FROM], function(database, tableArray) {
-                $.each(tableArray, function(i, table) {
-                    databaseAndTables.push(database + '.' + table);
-                });
-            });
-            $.unique(databaseAndTables);
-            finalQuery += '\n' + 'FROM ' + databaseAndTables.join(',\n     ');
-
-            // Build WHERE rules
-            var whereColumns = [];
-            $.each(rules[FILTER], function(index, value) {
-                // Check if the string is numeric
-                if (/^\d+$/.test(value.input)) {
-                    whereColumns.push(QueryBuilder.formatString(value.template, value.table, value.column, value.input));
-                } else {
-                    whereColumns.push(QueryBuilder.formatString(value.template, value.table, value.column, "'" + value.input + "'"));
+              }, _items[SELECT_DISTINCT] = {
+                "name": SELECT_DISTINCT,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, SELECT_DISTINCT, PROJECT, "{0}", false, false);
                 }
                 }
-            });
-            if (whereColumns.length > 0) {
-                finalQuery += '\n' + 'WHERE ' + whereColumns.join('\n  AND ');
-            }
-
-            // Build GROUP rules
-            // If we have any aggregates, group on all projected columns
-            if (aggregateColumns.length > 0) {
-                if (selectColumns.length > 0) {
-                    finalQuery += '\n' + 'GROUP BY ' + selectColumns.join(',\n         ');
+              }, _items)
+            }, _items5[AGGREGATE] = {
+              "name": AGGREGATE,
+              "items": (_items2 = {}, _items2[COUNT] = {
+                "name": COUNT,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, COUNT, AGGREGATE, "COUNT({0}.{1}) as count_{1}", false, false);
                 }
                 }
-            }
-
-            // Build ORDER rules
-            var orderColumns = [];
-            $.each(rules[ORDER], function(index, value) {
-                orderColumns.push(QueryBuilder.formatString(value.template, value.table, value.column));
-            });
-            if (orderColumns.length > 0) {
-                finalQuery += '\n' + 'ORDER BY ' + orderColumns.join(',\n         ');
-            }
-
-            // Add final semicolon
-            finalQuery += ';';
-
-            // Return the output query
-            return {
-                "status": "pass",
-                "query": finalQuery
-            };
-        }
-
-        /**
-         * Binds context menu to a provided selector class.
-         * Each object using the selector class is expected to have several attributes: 'database', 'table' and 'column'
-         * Each rule internally has a template that will be used. The format is '{0}' -> database, {1} -> table, {2} -> input
-         * @param {string} selectorClass The selector class to use.
-         */
-        QueryBuilder.bindMenu = function(selectorClass, callbackOnRuleAdd) {
-            QueryBuilder.callbackOnRuleAdd = callbackOnRuleAdd;
-            $.contextMenu({
-                selector: selectorClass,
-                trigger: 'right',
-                zIndex: 1000,
-                build: function($trigger, e) {
-                    // Get the database, table, and column
-                    var database = $trigger.attr('database');
-                    var table = $trigger.attr('table');
-                    var column = $trigger.attr('column');
-                    var result = {
-                        items: {
-                            "column-name": {
-                                name: column,
-                                disabled: true,
-                                className: 'muted'
-                            },
-                            "sep1": "---------",
-                            [PROJECT]: {
-                                "name": PROJECT,
-                                "items": {
-                                    [SELECT]: {
-                                        "name": SELECT,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, SELECT, PROJECT, "{0}", false, false);
-                                        },
-                                    },
-                                    [SELECT_DISTINCT]: {
-                                        "name": SELECT_DISTINCT,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, SELECT_DISTINCT, PROJECT, "{0}", false, false);
-                                        },
-                                    }
-                                }
-                            },
-                            [AGGREGATE]: {
-                                "name": AGGREGATE,
-                                "items": {
-                                    [COUNT]: {
-                                        "name": COUNT,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, COUNT, AGGREGATE, "COUNT({0}.{1}) as count_{1}", false, false);
-                                        },
-                                    },
-                                    [COUNT_DISTINCT]: {
-                                        "name": COUNT_DISTINCT,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, COUNT_DISTINCT, AGGREGATE, "COUNT(DISTINCT {0}.{1}) as distinct_count_{1}", false, false);
-                                        },
-                                    },
-                                    [SUM]: {
-                                        "name": SUM,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, SUM, AGGREGATE, "SUM({0}.{1}) as sum_{1}", false, false);
-                                        },
-                                    },
-                                    [MINIMUM]: {
-                                        "name": MINIMUM,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, MINIMUM, AGGREGATE, "MIN({0}.{1}) as min_{1}", false, false);
-                                        },
-                                    },
-                                    [MAXIMUM]: {
-                                        "name": MAXIMUM,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, MAXIMUM, AGGREGATE, "MAX({0}.{1}) as max_{1}", false, false);
-                                        },
-                                    },
-                                    [AVERAGE]: {
-                                        "name": AVERAGE,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, AVERAGE, AGGREGATE, "AVG({0}.{1}) as avg_{1}", false, false);
-                                        },
-                                    }
-                                }
-                            },
-                            [FILTER]: {
-                                "name": FILTER,
-                                "items": {
-                                    [IS_NULL]: {
-                                        "name": IS_NULL,
-                                        callback: function(key, options) {
-                                            // Check if there is a 'is not null' rule
-                                            var isNotNullRule = QueryBuilder.getRule(database, table, column, IS_NOT_NULL);
-                                            if (isNotNullRule.length) {
-                                                // Update it to be 'is null'
-                                                isNotNullRule.attr('rule', IS_NULL);
-                                                isNotNullRule.find('.rule').text(IS_NULL);
-                                            } else {
-                                                QueryBuilder.addRule(database, table, column, IS_NULL, FILTER, "{0}.{1} = null", false, false);
-                                            }
-                                        },
-                                    },
-                                    [IS_NOT_NULL]: {
-                                        "name": IS_NOT_NULL,
-                                        callback: function(key, options) {
-                                            // Check if there is a 'is null' rule
-                                            var isNullRule = QueryBuilder.getRule(database, table, column, IS_NULL);
-                                            if (isNullRule.length) {
-                                                // Update it to be 'is null'
-                                                isNullRule.attr('rule', IS_NOT_NULL);
-                                                isNullRule.find('.rule').text(IS_NOT_NULL);
-                                            } else {
-                                                QueryBuilder.addRule(database, table, column, IS_NOT_NULL, FILTER, "{0}.{1} != null", false, false);
-                                            }
-                                        },
-                                    },
-                                    [EQUAL_TO]: {
-                                        "name": EQUAL_TO,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, EQUAL_TO, FILTER, "{0}.{1} = {2}", true, true);
-                                        },
-                                    },
-                                    [NOT_EQUAL_TO]: {
-                                        "name": NOT_EQUAL_TO,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, NOT_EQUAL_TO, FILTER, "{0}.{1} != {2}", true, true);
-                                        },
-                                    },
-                                    [GREATER_THAN]: {
-                                        "name": GREATER_THAN,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, GREATER_THAN, FILTER, "{0}.{1} > {2}", true, false);
-                                        },
-                                    },
-                                    [LESS_THAN]: {
-                                        "name": LESS_THAN,
-                                        callback: function(key, options) {
-                                            QueryBuilder.addRule(database, table, column, LESS_THAN, FILTER, "{0}.{1} < {2}", true, false);
-                                        },
-                                    },
-                                },
-                            },
-                            [ORDER]: {
-                                "name": ORDER,
-                                "items": {
-                                    [ASCENDING]: {
-                                        "name": ASCENDING,
-                                        callback: function(key, options) {
-                                            // Check if there is a descending rule
-                                            var descendingRule = QueryBuilder.getRule(database, table, column, DESCENDING);
-                                            if (descendingRule.length) {
-                                                // Update it to be 'ascending'
-                                                descendingRule.attr('rule', ASCENDING);
-                                                descendingRule.find('.rule').text(ASCENDING);
-                                            } else {
-                                                QueryBuilder.addRule(database, table, column, ASCENDING, ORDER, "{0}.{1} ASC", false, false);
-                                            }
-                                        },
-                                    },
-                                    [DESCENDING]: {
-                                        "name": DESCENDING,
-                                        callback: function(key, options) {
-                                            // Check if there is a ascending rule
-                                            var ascendingRule = QueryBuilder.getRule(database, table, column, ASCENDING);
-                                            if (ascendingRule.length) {
-                                                // Update it to be 'descending'
-                                                ascendingRule.attr('rule', DESCENDING);
-                                                ascendingRule.find('.rule').text(DESCENDING);
-                                            } else {
-                                                QueryBuilder.addRule(database, table, column, DESCENDING, ORDER, "{0}.{1} DESC", false, false);
-                                            }
-                                        },
-                                    }
-                                }
-                            }
-                        }
-                    };
-
-                    // If select rule used, remove project and aggregates
-                    if (QueryBuilder.countRulesByCategory(database, table, column, PROJECT)) {
-                        delete result.items[PROJECT];
-                        delete result.items[AGGREGATE];
-                    }
-
-                    // If aggretes rule(s) used, remove project
-                    if (QueryBuilder.countRulesByCategory(database, table, column, AGGREGATE)) {
-                        delete result.items[PROJECT];
-                    }
-
-                    return result;
+              }, _items2[COUNT_DISTINCT] = {
+                "name": COUNT_DISTINCT,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, COUNT_DISTINCT, AGGREGATE, "COUNT(DISTINCT {0}.{1}) as distinct_count_{1}", false, false);
+                }
+              }, _items2[SUM] = {
+                "name": SUM,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, SUM, AGGREGATE, "SUM({0}.{1}) as sum_{1}", false, false);
+                }
+              }, _items2[MINIMUM] = {
+                "name": MINIMUM,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, MINIMUM, AGGREGATE, "MIN({0}.{1}) as min_{1}", false, false);
+                }
+              }, _items2[MAXIMUM] = {
+                "name": MAXIMUM,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, MAXIMUM, AGGREGATE, "MAX({0}.{1}) as max_{1}", false, false);
+                }
+              }, _items2[AVERAGE] = {
+                "name": AVERAGE,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, AVERAGE, AGGREGATE, "AVG({0}.{1}) as avg_{1}", false, false);
+                }
+              }, _items2)
+            }, _items5[FILTER] = {
+              "name": FILTER,
+              "items": (_items3 = {}, _items3[IS_NULL] = {
+                "name": IS_NULL,
+                callback: function callback(key, options) {
+                  // Check if there is a 'is not null' rule
+                  var isNotNullRule = QueryBuilder.getRule(database, table, column, IS_NOT_NULL);
+                  if (isNotNullRule.length) {
+                    // Update it to be 'is null'
+                    isNotNullRule.attr('rule', IS_NULL);
+                    isNotNullRule.find('.rule').text(IS_NULL);
+                  } else {
+                    QueryBuilder.addRule(database, table, column, IS_NULL, FILTER, "{0}.{1} = null", false, false);
+                  }
                 }
                 }
-            });
+              }, _items3[IS_NOT_NULL] = {
+                "name": IS_NOT_NULL,
+                callback: function callback(key, options) {
+                  // Check if there is a 'is null' rule
+                  var isNullRule = QueryBuilder.getRule(database, table, column, IS_NULL);
+                  if (isNullRule.length) {
+                    // Update it to be 'is null'
+                    isNullRule.attr('rule', IS_NOT_NULL);
+                    isNullRule.find('.rule').text(IS_NOT_NULL);
+                  } else {
+                    QueryBuilder.addRule(database, table, column, IS_NOT_NULL, FILTER, "{0}.{1} != null", false, false);
+                  }
+                }
+              }, _items3[EQUAL_TO] = {
+                "name": EQUAL_TO,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, EQUAL_TO, FILTER, "{0}.{1} = {2}", true, true);
+                }
+              }, _items3[NOT_EQUAL_TO] = {
+                "name": NOT_EQUAL_TO,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, NOT_EQUAL_TO, FILTER, "{0}.{1} != {2}", true, true);
+                }
+              }, _items3[GREATER_THAN] = {
+                "name": GREATER_THAN,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, GREATER_THAN, FILTER, "{0}.{1} > {2}", true, false);
+                }
+              }, _items3[LESS_THAN] = {
+                "name": LESS_THAN,
+                callback: function callback(key, options) {
+                  QueryBuilder.addRule(database, table, column, LESS_THAN, FILTER, "{0}.{1} < {2}", true, false);
+                }
+              }, _items3)
+            }, _items5[ORDER] = {
+              "name": ORDER,
+              "items": (_items4 = {}, _items4[ASCENDING] = {
+                "name": ASCENDING,
+                callback: function callback(key, options) {
+                  // Check if there is a descending rule
+                  var descendingRule = QueryBuilder.getRule(database, table, column, DESCENDING);
+                  if (descendingRule.length) {
+                    // Update it to be 'ascending'
+                    descendingRule.attr('rule', ASCENDING);
+                    descendingRule.find('.rule').text(ASCENDING);
+                  } else {
+                    QueryBuilder.addRule(database, table, column, ASCENDING, ORDER, "{0}.{1} ASC", false, false);
+                  }
+                }
+              }, _items4[DESCENDING] = {
+                "name": DESCENDING,
+                callback: function callback(key, options) {
+                  // Check if there is a ascending rule
+                  var ascendingRule = QueryBuilder.getRule(database, table, column, ASCENDING);
+                  if (ascendingRule.length) {
+                    // Update it to be 'descending'
+                    ascendingRule.attr('rule', DESCENDING);
+                    ascendingRule.find('.rule').text(DESCENDING);
+                  } else {
+                    QueryBuilder.addRule(database, table, column, DESCENDING, ORDER, "{0}.{1} DESC", false, false);
+                  }
+                }
+              }, _items4)
+            }, _items5)
+          };
+
+          // If select rule used, remove project and aggregates
+          if (QueryBuilder.countRulesByCategory(database, table, column, PROJECT)) {
+            delete result.items[PROJECT];
+            delete result.items[AGGREGATE];
+          }
+
+          // If aggretes rule(s) used, remove project
+          if (QueryBuilder.countRulesByCategory(database, table, column, AGGREGATE)) {
+            delete result.items[PROJECT];
+          }
+
+          return result;
         }
         }
-
-        return QueryBuilder;
-    }
-
-    // Define QueryBuilder globally is it does not already exist
-    if (typeof(QueryBuilder) === 'undefined') {
-        window.QueryBuilder = defineQueryBuilder();
-    } else {
-        console.log("QueryBuilder already defined.");
-    }
-})(window);
+      });
+    };
+
+    return QueryBuilder;
+  }
+
+  // Define QueryBuilder globally is it does not already exist
+  if (typeof QueryBuilder === 'undefined') {
+    window.QueryBuilder = defineQueryBuilder();
+  } else {
+    console.log("QueryBuilder already defined.");
+  }
+})(window);