UNPKG

@react-query-builder-express/core

Version:
356 lines (311 loc) 10.5 kB
import {getWidgetForFieldOp} from "../utils/ruleUtils"; import {defaultConjunction} from "../utils/defaultUtils"; import { extendConfig } from "../utils/configUtils"; /** * Converts a string representation of top_left and bottom_right cords to * a ES geo_point required for query * * @param {string} geoPointString - comma separated string of lat/lon coods * @returns {{top_left: {lon: number, lat: number}, bottom_right: {lon: number, lat: number}}} - ES geoPoint formatted object * @private */ function buildEsGeoPoint(geoPointString) { if (geoPointString == null) { return null; } const coordsNumberArray = geoPointString.split(",").map(Number); return { top_left: { lat: coordsNumberArray[0], lon: coordsNumberArray[1] }, bottom_right: { lat: coordsNumberArray[2], lon: coordsNumberArray[3] } }; } /** * Converts a dateTime string from the query builder to a ES range formatted object * * @param {string} dateTime - dateTime formatted string * @param {string} operator - query builder operator type, see constants.js and query builder docs * @returns {{lt: string}|{lte: string}|{gte: string}|{gte: string, lte: string}|undefined} - ES range query parameter * * @private */ function buildEsRangeParameters(value, operator) { // -- if value is greater than 1 then we assume this is a between operator : BUG this is wrong, a selectable list can have multiple values if (value.length > 1) { return { gte: "".concat(value[0]), lte: "".concat(value[1]) }; } // -- if value is only one we assume this is a date time query for a specific day const dateTime = value[0]; //TODO: Rethink about this part, what if someone adds a new type of opperator //todo: move this logic into config switch (operator) { case "on_date": //todo: not used case "not_on_date": case "equal": case "select_equals": case "not_equal": return { gte: "".concat(dateTime, "||/d"), lte: "".concat(dateTime, "||+1d") }; case "less_or_equal": return { lte: "".concat(dateTime) }; case "greater_or_equal": return { gte: "".concat(dateTime) }; case "less": return { lt: "".concat(dateTime) }; case "greater": return { gt: "".concat(dateTime) }; default: return undefined; } } /** * Builds the DSL parameters for a Wildcard query * * @param {string} value - The match value * @returns {{value: string}} - The value = value parameter surrounded with * on each end * @private */ function buildEsWildcardParameters(value) { return { value: "*" + value + "*" }; } /** * Takes the match type string from awesome query builder like 'greater_or_equal' and * returns the ES occurrence required for bool queries * * @param {string} combinator - query group type or rule condition * @param {bool} not * @returns {string} - ES occurrence type. See constants.js * @private */ function determineOccurrence(combinator, not) { //todo: move into config, like mongoConj switch (combinator) { case "AND": return not ? "must_not" : "must"; // -- AND case "OR": return not ? "should_not" : "should"; // -- OR case "NOT": return not ? "must" : "must_not"; // -- NOT AND default: return undefined; } } /** * Determines what field to query off of given the operator type * * @param {string} fieldDataType - The type of data * @param {string} fullFieldName - A '.' separated string containing the property lineage (including self) * @param {string} queryType - The query type * @returns {string|*} - will be either the fullFieldName or fullFieldName.keyword * @private */ //todo: not used // function determineQueryField(fieldDataType, fullFieldName, queryType) { // if (fieldDataType === "boolean") { // return fullFieldName; // } // switch (queryType) { // case "term": // case "wildcard": // return "".concat(fullFieldName, ".keyword"); // case "geo_bounding_box": // case "range": // case "match": // return fullFieldName; // default: // console.error("Can't determine query field for query type ".concat(queryType)); // return null; // } // } function buildRegexpParameters(value) { return { value: value }; } function determineField(fieldName, config) { //todo: ElasticSearchTextField - not used //return config.fields[fieldName].ElasticSearchTextField || fieldName; return fieldName; } function buildParameters(queryType, value, operator, fieldName, config, syntax) { const textField = determineField(fieldName, config); switch (queryType) { case "filter": //todo: elasticSearchScript - not used return { script: config.operators[operator].elasticSearchScript(fieldName, value) }; case "exists": return { field: fieldName }; case "match": return { [textField]: value[0] }; case "term": return syntax === ES_7_SYNTAX ? { [fieldName]: { value: value[0] }} : { [fieldName]: value[0] }; //todo: not used // need to add geo type into RAQB or remove this code case "geo_bounding_box": return { [fieldName]: buildEsGeoPoint(value[0]) }; case "range": return { [fieldName]: buildEsRangeParameters(value, operator) }; case "wildcard": return { [fieldName]: buildEsWildcardParameters(value[0]) }; case "regexp": return { [fieldName]: buildRegexpParameters(value[0]) }; default: return undefined; } } /** * Handles the building of the group portion of the DSL * * @param {string} fieldName - The name of the field you are building a rule for * @param {string} fieldDataType - The type of data this field holds * @param {string} value - The value of this rule * @param {string} operator - The condition on how the value is matched * @param {string} syntax - The version of ElasticSearch syntax to generate * @returns {object} - The ES rule * @private */ function buildEsRule(fieldName, value, operator, config, valueSrc, syntax) { if (!fieldName || !operator || value == undefined) return undefined; // rule is not fully entered let op = operator; let opConfig = config.operators[op]; if (!opConfig) return undefined; // unknown operator let { elasticSearchQueryType } = opConfig; // not let not = false; if (!elasticSearchQueryType && opConfig.reversedOp) { not = true; op = opConfig.reversedOp; opConfig = config.operators[op]; ({ elasticSearchQueryType } = opConfig); } // handle if value 0 has multiple values like a select in a array const widget = getWidgetForFieldOp(config, fieldName, op, valueSrc); const widgetConfig = config.widgets[widget]; if (!widgetConfig) return undefined; // unknown widget const { elasticSearchFormatValue } = widgetConfig; /** In most cases the queryType will be static however in some casese (like between) the query type will change * based on the data type. i.e. a between time will be different than between number, date, letters etc... */ let queryType; if (typeof elasticSearchQueryType === "function") { queryType = elasticSearchQueryType(widget); } else { queryType = elasticSearchQueryType; } if (!queryType) { // Not supported return undefined; } /** If a widget has a rule on how to format that data then use that otherwise use default way of determineing search parameters * */ let parameters; if (typeof elasticSearchFormatValue === "function") { parameters = elasticSearchFormatValue(queryType, value, op, fieldName, config); } else { parameters = buildParameters(queryType, value, op, fieldName, config, syntax); } if (not) { return { bool: { must_not: { [queryType]: {...parameters} } } }; } else { return { [queryType]: {...parameters} }; } } /** * Handles the building of the group portion of the DSL * * @param {object} children - The contents of the group * @param {string} conjunction - The way the contents of the group are joined together i.e. AND OR * @param {bool} not * @param {Function} recursiveFxn - The recursive fxn to build the contents of the groups children * @private * @returns {object} - The ES group */ function buildEsGroup(children, conjunction, not, recursiveFxn, config, syntax) { if (!children || !children.size) return undefined; const childrenArray = children.valueSeq().toArray(); const occurrence = determineOccurrence(conjunction, not); const result = childrenArray.map((c) => recursiveFxn(c, config, syntax)).filter(v => v !== undefined); if (!result.length) return undefined; const resultFlat = result.flat(Infinity); return { bool: { [occurrence]: resultFlat } }; } export const ES_7_SYNTAX = "ES_7_SYNTAX"; export const ES_6_SYNTAX = "ES_6_SYNTAX"; export function elasticSearchFormat(tree, config, syntax = ES_6_SYNTAX) { const extendedConfig = extendConfig(config, undefined, false); // -- format the es dsl here if (!tree) return undefined; const type = tree.get("type"); const properties = tree.get("properties") || new Map(); if (type === "rule" && properties.get("field")) { // -- field is null when a new blank rule is added const operator = properties.get("operator"); const field = properties.get("field"); const fieldSrc = properties.get("fieldSrc"); const value = properties.get("value")?.toJS(); const _valueType = properties.get("valueType")?.get(0); const valueSrc = properties.get("valueSrc")?.get(0); if (valueSrc === "func" || fieldSrc == "func") { // -- elastic search doesn't support functions (that is post processing) return; } if (value && Array.isArray(value[0])) { //TODO : Handle case where the value has multiple values such as in the case of a list return value[0].map((val) => buildEsRule(field, [val], operator, extendedConfig, valueSrc, syntax) ); } else { return buildEsRule(field, value, operator, extendedConfig, valueSrc, syntax); } } if (type === "group" || type === "rule_group") { const not = properties.get("not"); let conjunction = properties.get("conjunction"); if (!conjunction) conjunction = defaultConjunction(extendedConfig); const children = tree.get("children1"); return buildEsGroup(children, conjunction, not, elasticSearchFormat, extendedConfig, syntax); } }