UNPKG

@react-awesome-query-builder/core

Version:
1,485 lines (1,467 loc) 59 kB
import {settings as defaultSettings} from "./default"; import ctx from "./ctx"; //---------------------------- conjunctions const conjunctions = { AND: { label: "And", mongoConj: "$and", jsonLogicConj: "and", sqlConj: "AND", spelConj: "and", spelConjs: ["and", "&&"], reversedConj: "OR", formatConj: function (children, conj, not, isForDisplay) { let ret = children.size > 1 ? children.join(" " + (isForDisplay ? "AND" : "&&") + " ") : children.first(); if (children.size > 1 || not) { ret = this.utils.wrapWithBrackets(ret); } if (not) { ret = "NOT " + ret; } return ret; }, sqlFormatConj: function (children, conj, not) { let ret = children.size > 1 ? children.join(" " + "AND" + " ") : children.first(); if (children.size > 1 || not) { ret = this.utils.wrapWithBrackets(ret); } if (not) { ret = "NOT " + ret; } return ret; }, spelFormatConj: function (children, conj, not, omitBrackets) { if (not) omitBrackets = false; let ret = children.size > 1 ? children.join(" " + "&&" + " ") : children.first(); if ((children.size > 1 || not) && !omitBrackets) { ret = this.utils.wrapWithBrackets(ret); } if (not) { ret = "!" + ret; } return ret; }, }, OR: { label: "Or", mongoConj: "$or", jsonLogicConj: "or", sqlConj: "OR", spelConj: "or", spelConjs: ["or", "||"], reversedConj: "AND", formatConj: (children, conj, not, isForDisplay) => { return children.size > 1 ? (not ? "NOT " : "") + "(" + children.join(" " + (isForDisplay ? "OR" : "||") + " ") + ")" : (not ? "NOT (" : "") + children.first() + (not ? ")" : ""); }, sqlFormatConj: function (children, conj, not) { let ret = (children.size > 1 ? children.join(" " + "OR" + " ") : children.first()); if (children.size > 1 || not) { ret = this.utils.wrapWithBrackets(ret); } if (not) { ret = "NOT " + ret; } return ret; }, spelFormatConj: (children, conj, not, omitBrackets) => { if (not) omitBrackets = false; return children.size > 1 ? (not ? "!" : "") + (omitBrackets ? "" : "(") + children.join(" " + "||" + " ") + (omitBrackets ? "" : ")") : (not ? "!(" : "") + children.first() + (not ? ")" : ""); }, }, }; //---------------------------- operators const operators = { equal: { label: "==", labelForFormat: "==", sqlOp: "=", spelOp: "==", spelOps: ["==", "eq"], reversedOp: "not_equal", formatOp: (field, op, value, valueSrcs, valueTypes, opDef, operatorOptions, isForDisplay, fieldDef) => { const opStr = isForDisplay ? "=" : opDef.label; if (valueTypes == "boolean" && isForDisplay) return value == "No" ? `NOT ${field}` : `${field}`; else return `${field} ${opStr} ${value}`; }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$eq", v => v, false, ...args); }, jsonLogic2: "==", jsonLogicOps: ["==", "datetime==", "date=="], jsonLogic: (field, op, val, _opDef, _opOpts, _fieldDef, expectedType, settings) => { if (settings?.fixJsonLogicDateCompareOp && ["date", "datetime"].includes(expectedType)) { return { [`${expectedType}==`]: [field, val] }; } return { "==": [field, val] }; }, elasticSearchQueryType: "term", }, not_equal: { isNotOp: true, label: "!=", labelForFormat: "!=", sqlOp: "<>", sqlOps: ["<>", "!="], spelOp: "!=", spelOps: ["!=", "ne"], reversedOp: "equal", formatOp: (field, op, value, valueSrcs, valueTypes, opDef, operatorOptions, isForDisplay, fieldDef) => { if (valueTypes == "boolean" && isForDisplay) return value == "No" ? `${field}` : `NOT ${field}`; else return `${field} ${opDef.label} ${value}`; }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$ne", v => v, false, ...args); }, jsonLogic2: "!=", jsonLogicOps: ["!=", "datetime!=", "date!="], jsonLogic: (field, op, val, _opDef, _opOpts, _fieldDef, expectedType, settings) => { if (settings?.fixJsonLogicDateCompareOp && ["date", "datetime"].includes(expectedType)) { return { [`${expectedType}!=`]: [field, val] }; } return { "!=": [field, val] }; }, }, less: { label: "<", labelForFormat: "<", sqlOp: "<", spelOp: "<", spelOps: ["<", "lt"], reversedOp: "greater_or_equal", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$lt", v => v, false, ...args); }, jsonLogic: "<", elasticSearchQueryType: "range", }, less_or_equal: { label: "<=", labelForFormat: "<=", sqlOp: "<=", spelOp: "<=", spelOps: ["<=", "le"], reversedOp: "greater", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$lte", v => v, false, ...args); }, jsonLogic: "<=", elasticSearchQueryType: "range", }, greater: { label: ">", labelForFormat: ">", sqlOp: ">", spelOp: ">", spelOps: [">", "gt"], reversedOp: "less_or_equal", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$gt", v => v, false, ...args); }, jsonLogic: ">", elasticSearchQueryType: "range", }, greater_or_equal: { label: ">=", labelForFormat: ">=", sqlOp: ">=", spelOp: ">=", spelOps: [">=", "ge"], reversedOp: "less", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$gte", v => v, false, ...args); }, jsonLogic: ">=", elasticSearchQueryType: "range", }, like: { label: "Contains", labelForFormat: "Contains", reversedOp: "not_like", sqlOp: "LIKE", // tip: this function covers import of 3 operators sqlImport: function (sqlObj, _, sqlDialect) { if (sqlObj?.operator == "LIKE" || sqlObj?.operator == "NOT LIKE") { const not = sqlObj?.operator == "NOT LIKE"; const [_left, right] = sqlObj.children || []; if (right?.valueType?.endsWith("_quote_string")) { if (right?.value.startsWith("%") && right?.value.endsWith("%")) { right.value = this.utils.SqlString.unescapeLike(right.value.substring(1, right.value.length - 1), sqlDialect); sqlObj.operator = not ? "not_like" : "like"; return sqlObj; } else if (right?.value.startsWith("%")) { right.value = this.utils.SqlString.unescapeLike(right.value.substring(1), sqlDialect); sqlObj.operator = "ends_with"; return sqlObj; } else if (right?.value.endsWith("%")) { right.value = this.utils.SqlString.unescapeLike(right.value.substring(0, right.value.length - 1), sqlDialect); sqlObj.operator = "starts_with"; return sqlObj; } } } }, spelOp: "${0}.contains(${1})", valueTypes: ["text"], mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$regex", v => (typeof v == "string" ? this.utils.escapeRegExp(v) : undefined), false, ...args); }, jsonLogic: (field, op, val) => ({ "in": [val, field] }), jsonLogic2: "#in", valueSources: ["value"], elasticSearchQueryType: "regexp", }, not_like: { isNotOp: true, label: "Not contains", reversedOp: "like", labelForFormat: "Not Contains", sqlOp: "NOT LIKE", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$regex", v => (typeof v == "string" ? this.utils.escapeRegExp(v) : undefined), true, ...args); }, jsonLogic: (field, op, val) => ({"!": { "in": [val, field] }}), jsonLogic2: "#!in", _jsonLogicIsExclamationOp: true, valueSources: ["value"], }, starts_with: { label: "Starts with", labelForFormat: "Starts with", sqlOp: "LIKE", spelOp: "${0}.startsWith(${1})", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$regex", v => (typeof v == "string" ? "^" + this.utils.escapeRegExp(v) : undefined), false, ...args); }, jsonLogic: undefined, // not supported valueSources: ["value"], }, ends_with: { label: "Ends with", labelForFormat: "Ends with", sqlOp: "LIKE", spelOp: "${0}.endsWith(${1})", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$regex", v => (typeof v == "string" ? this.utils.escapeRegExp(v) + "$" : undefined), false, ...args); }, jsonLogic: undefined, // not supported valueSources: ["value"], }, between: { label: "Between", labelForFormat: "BETWEEN", sqlOp: "BETWEEN", cardinality: 2, formatOp: (field, op, values, valueSrcs, valueTypes, opDef, operatorOptions, isForDisplay) => { let valFrom = values.first(); let valTo = values.get(1); if (isForDisplay) return `${field} BETWEEN ${valFrom} AND ${valTo}`; else return `${field} >= ${valFrom} && ${field} <= ${valTo}`; }, // tip: this op can be imported from SpEL manually without using config spelFormatOp: (field, op, values, valueSrc, valueTypes, opDef, operatorOptions, fieldDef) => { const valFrom = values[0]; const valTo = values[1]; return `(${field} >= ${valFrom} && ${field} <= ${valTo})`; }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp2(["$gte", "$lte"], false, ...args); }, valueLabels: [ "Value from", "Value to" ], textSeparators: [ null, "and" ], reversedOp: "not_between", jsonLogic: "<=", validateValues: (values) => { if (values[0] != undefined && values[1] != undefined) { return values[0] <= values[1]; } return null; }, elasticSearchQueryType: function elasticSearchQueryType(type) { return type === "time" ? "filter" : "range"; }, }, not_between: { isNotOp: true, label: "Not between", labelForFormat: "NOT BETWEEN", sqlOp: "NOT BETWEEN", cardinality: 2, formatOp: (field, op, values, valueSrcs, valueTypes, opDef, operatorOptions, isForDisplay) => { let valFrom = values.first(); let valTo = values.get(1); if (isForDisplay) return `${field} NOT BETWEEN ${valFrom} AND ${valTo}`; else return `(${field} < ${valFrom} || ${field} > ${valTo})`; }, spelFormatOp: (field, op, values, valueSrc, valueTypes, opDef, operatorOptions, fieldDef) => { const valFrom = values[0]; const valTo = values[1]; return `(${field} < ${valFrom} || ${field} > ${valTo})`; }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp2(["$gte", "$lte"], true, ...args); }, valueLabels: [ "Value from", "Value to" ], textSeparators: [ null, "and" ], reversedOp: "between", jsonLogic: (field, op, val) => ({"!": { "<=": [Array.isArray(val) ? val[0] : val, field, Array.isArray(val) ? val[1] : val] }}), jsonLogic2: "!<=", _jsonLogicIsExclamationOp: true, validateValues: (values) => { if (values[0] != undefined && values[1] != undefined) { return values[0] <= values[1]; } return null; }, }, is_empty: { label: "Is empty", labelForFormat: "IS EMPTY", cardinality: 0, reversedOp: "is_not_empty", formatOp: (field, op, value, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { return isForDisplay ? `${field} IS EMPTY` : `!${field}`; }, sqlFormatOp: function (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) { const empty = this.utils.sqlEmptyValue(fieldDef); return `COALESCE(${field}, ${empty}) = ${empty}`; }, // tip: this function covers import of 2 operators sqlImport: function (sqlObj, _, sqlDialect) { if (sqlObj?.operator === "=" || sqlObj?.operator === "<>") { const [left, right] = sqlObj.children || []; if (right?.value === "" && left?.func === "COALESCE" && left?.children?.[1]?.value === "") { sqlObj.operator = sqlObj?.operator === "=" ? "is_empty" : "is_not_empty"; sqlObj.children = [ left.children[0] ]; return sqlObj; } } }, // tip: this op can be imported from SpEL manually without using config spelFormatOp: (field, op, values, valueSrc, valueTypes, opDef, operatorOptions, fieldDef) => { //tip: is empty or null return `${field} <= ''`; }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$in", (v, fieldDef) => [this.utils.mongoEmptyValue(fieldDef), null], false, ...args); }, jsonLogic: "!", }, is_not_empty: { isNotOp: true, label: "Is not empty", labelForFormat: "IS NOT EMPTY", cardinality: 0, reversedOp: "is_empty", formatOp: (field, op, value, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { return isForDisplay ? `${field} IS NOT EMPTY` : `!!${field}`; }, sqlFormatOp: function (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) { const empty = this.utils.sqlEmptyValue(fieldDef); return `COALESCE(${field}, ${empty}) <> ${empty}`; }, spelFormatOp: (field, op, values, valueSrc, valueTypes, opDef, operatorOptions, fieldDef) => { //tip: is not empty and not null return `${field} > ''`; }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$nin", (v, fieldDef) => [this.utils.mongoEmptyValue(fieldDef), null], false, ...args); }, jsonLogic: "!!", elasticSearchQueryType: "exists", }, is_null: { label: "Is null", labelForFormat: "IS NULL", sqlOp: "IS NULL", // tip: this function covers import of 2 operators sqlImport: function (sqlObj, _, sqlDialect) { if (sqlObj?.operator === "IS" || sqlObj?.operator === "IS NOT") { const [left, right] = sqlObj.children || []; if (right?.valueType == "null") { sqlObj.operator = sqlObj?.operator === "IS" ? "is_null" : "is_not_null"; sqlObj.value = left; return sqlObj; } } }, cardinality: 0, reversedOp: "is_not_null", formatOp: (field, op, value, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { return isForDisplay ? `${field} IS NULL` : `!${field}`; }, // tip: this op can be imported from SpEL manually without using config spelFormatOp: (field, op, values, valueSrc, valueTypes, opDef, operatorOptions, fieldDef) => { return `${field} == null`; }, // check if value is null OR not exists mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$eq", v => null, false, ...args); }, jsonLogic: "==", }, is_not_null: { label: "Is not null", labelForFormat: "IS NOT NULL", sqlOp: "IS NOT NULL", cardinality: 0, reversedOp: "is_null", formatOp: (field, op, value, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { return isForDisplay ? `${field} IS NOT NULL` : `!!${field}`; }, spelFormatOp: (field, op, values, valueSrc, valueTypes, opDef, operatorOptions, fieldDef) => { return `${field} != null`; }, // check if value exists and is not null mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$ne", v => null, false, ...args); }, jsonLogic: "!=", elasticSearchQueryType: "exists", }, select_equals: { label: "==", labelForFormat: "==", sqlOp: "=", // enum/set formatOp: (field, op, value, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { const opStr = isForDisplay ? "=" : "=="; return `${field} ${opStr} ${value}`; }, spelOp: "==", spelOps: ["==", "eq"], mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$eq", v => v, false, ...args); }, reversedOp: "select_not_equals", jsonLogic: "==", elasticSearchQueryType: "term", }, select_not_equals: { isNotOp: true, label: "!=", labelForFormat: "!=", sqlOp: "<>", // enum/set sqlOps: ["<>", "!="], formatOp: (field, op, value, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { return `${field} != ${value}`; }, spelOp: "!=", spelOps: ["!=", "ne"], mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$ne", v => v, false, ...args); }, reversedOp: "select_equals", jsonLogic: "!=", }, select_any_in: { label: "Any in", labelForFormat: "IN", sqlOp: "IN", formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { if (valueSrc == "value") return `${field} IN (${values.join(", ")})`; else return `${field} IN (${values})`; }, sqlFormatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) => { if (valueSrc == "value") { return `${field} IN (${values.join(", ")})`; } else return undefined; // not supported }, valueTypes: ["multiselect"], spelOp: "${1}.contains(${0})", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$in", v => v, false, ...args); }, reversedOp: "select_not_any_in", jsonLogic: "in", elasticSearchQueryType: "term", }, select_not_any_in: { isNotOp: true, label: "Not in", labelForFormat: "NOT IN", sqlOp: "NOT IN", formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { if (valueSrc == "value") return `${field} NOT IN (${values.join(", ")})`; else return `${field} NOT IN (${values})`; }, sqlFormatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) => { if (valueSrc == "value") { return `${field} NOT IN (${values.join(", ")})`; } else return undefined; // not supported }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$nin", v => v, false, ...args); }, reversedOp: "select_any_in", jsonLogic: (field, op, val) => ({"!": { "in": [field, val] }}), jsonLogic2: "!in", _jsonLogicIsExclamationOp: true, }, // it's not "contains all", but "contains any" operator multiselect_contains: { label: "Contains", labelForFormat: "CONTAINS", valueTypes: ["multiselect"], formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { if (valueSrc == "value") return `${field} CONTAINS [${values.join(", ")}]`; else return `${field} CONTAINS ${values}`; }, reversedOp: "multiselect_not_contains", jsonLogic2: "some-in", jsonLogic: (field, op, vals) => ({ "some": [ field, {"in": [{"var": ""}, vals]} ] }), //spelOp: "${0}.containsAll(${1})", spelOp: "T(CollectionUtils).containsAny(${0}, ${1})", spelImportFuncs: [ // just for backward compatibility (issue #1007) { obj: { type: "property", val: "CollectionUtils" }, methodName: "containsAny", args: [ {var: "0"}, {var: "1"}, ], } ], elasticSearchQueryType: "term", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$in", v => v, false, ...args); }, }, multiselect_not_contains: { isNotOp: true, label: "Not contains", labelForFormat: "NOT CONTAINS", valueTypes: ["multiselect"], formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { if (valueSrc == "value") return `${field} NOT CONTAINS [${values.join(", ")}]`; else return `${field} NOT CONTAINS ${values}`; }, reversedOp: "multiselect_contains", jsonLogic2: "!some-in", jsonLogic: (field, op, vals) => ({ "!": { "some": [ field, {"in": [{"var": ""}, vals]} ]} }), _jsonLogicIsExclamationOp: true, }, multiselect_equals: { label: "Equals", labelForFormat: "==", sqlOp: "=", valueTypes: ["multiselect"], formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { const opStr = isForDisplay ? "=" : "=="; if (valueSrc == "value") return `${field} ${opStr} [${values.join(", ")}]`; else return `${field} ${opStr} ${values}`; }, sqlFormatOp: function (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) { if (valueSrc == "value") // set return `${field} = '${values.map(v => this.utils.SqlString.trim(v)).join(",")}'`; else return undefined; //not supported }, spelOp: "${0}.equals(${1})", mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$eq", v => v, false, ...args); }, reversedOp: "multiselect_not_equals", jsonLogic2: "all-in", jsonLogic: (field, op, vals) => ({ // it's not "equals", but "includes" operator - just for example "all": [ field, {"in": [{"var": ""}, vals]} ] }), elasticSearchQueryType: "term", }, multiselect_not_equals: { isNotOp: true, label: "Not equals", labelForFormat: "!=", sqlOp: "<>", sqlOps: ["<>", "!="], valueTypes: ["multiselect"], formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { if (valueSrc == "value") return `${field} != [${values.join(", ")}]`; else return `${field} != ${values}`; }, sqlFormatOp: function (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) { if (valueSrc == "value") // set return `${field} != '${values.map(v => this.utils.SqlString.trim(v)).join(",")}'`; else return undefined; //not supported }, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$ne", v => v, false, ...args); }, reversedOp: "multiselect_equals", jsonLogic2: "!all-in", jsonLogic: (field, op, vals) => ({ // it's not "equals", but "includes" operator - just for example "!": { "all": [ field, {"in": [{"var": ""}, vals]} ]} }), _jsonLogicIsExclamationOp: true, }, proximity: { label: "Proximity search", cardinality: 2, valueLabels: [ { label: "Word 1", placeholder: "Enter first word" }, { label: "Word 2", placeholder: "Enter second word" }, ], textSeparators: [ //'Word 1', //'Word 2' ], formatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => { const val1 = values.first(); const val2 = values.get(1); const prox = operatorOptions?.get("proximity"); return `${field} ${val1} NEAR/${prox} ${val2}`; }, sqlFormatOp: function (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) { // https://learn.microsoft.com/en-us/sql/relational-databases/search/search-for-words-close-to-another-word-with-near?view=sql-server-ver16#example-1 const val1 = values.first(); const val2 = values.get(1); const aVal1 = this.utils.SqlString.trim(val1); const aVal2 = this.utils.SqlString.trim(val2); const prox = operatorOptions?.get("proximity"); return `CONTAINS(${field}, 'NEAR((${aVal1}, ${aVal2}), ${prox})')`; }, sqlImport: function (sqlObj, _, sqlDialect) { if (sqlObj?.func === "CONTAINS") { const [left, right] = sqlObj.children || []; if (right?.value?.includes("NEAR(")) { const m = right.value.match(/NEAR\(\((\w+), (\w+)\), (\d+)\)/); if (m) { delete sqlObj.func; sqlObj.operator = "proximity"; sqlObj.children = [ left, { value: m[1] }, { value: m[2] }, ]; sqlObj.operatorOptions = { proximity: parseInt(m[3]) }; return sqlObj; } } } }, mongoFormatOp: undefined, // not supported jsonLogic: undefined, // not supported options: { optionLabel: "Near", // label on top of "near" selectbox (for config.settings.showLabels==true) optionTextBefore: "Near", // label before "near" selectbox (for config.settings.showLabels==false) optionPlaceholder: "Select words between", // placeholder for "near" selectbox minProximity: 2, maxProximity: 10, defaults: { proximity: 2 }, }, }, some: { label: "Some", labelForFormat: "SOME", cardinality: 0, jsonLogic: "some", spelFormatOp: (filteredSize) => `${filteredSize} > 0`, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$gt", v => 0, false, ...args); }, // reversedOp: undefined, }, all: { label: "All", labelForFormat: "ALL", cardinality: 0, jsonLogic: "all", spelFormatOp: (filteredSize, op, fullSize) => `${filteredSize} == ${fullSize}`, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$eq", v => v, false, ...args); }, // reversedOp: "none", }, none: { label: "None", labelForFormat: "NONE", cardinality: 0, jsonLogic: "none", spelFormatOp: (filteredSize) => `${filteredSize} == 0`, mongoFormatOp: function(...args) { return this.utils.mongoFormatOp1("$eq", v => 0, false, ...args); }, // reversedOp: "all", } }; //---------------------------- widgets const widgets = { text: { type: "text", jsType: "string", valueSrc: "value", valueLabel: "String", valuePlaceholder: "Enter string", formatValue: function (val, fieldDef, wgtDef, isForDisplay) { return isForDisplay ? this.utils.stringifyForDisplay(val) : JSON.stringify(val); }, spelFormatValue: function (val, fieldDef, wgtDef, op, opDef) { return this.utils.spelEscape(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { if (opDef.sqlOp == "LIKE" || opDef.sqlOp == "NOT LIKE") { return this.utils.SqlString.escapeLike(val, op != "starts_with", op != "ends_with", sqlDialect); } else { return this.utils.SqlString.escape(val); } }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, textarea: { type: "text", jsType: "string", valueSrc: "value", valueLabel: "Text", valuePlaceholder: "Enter text", formatValue: function (val, fieldDef, wgtDef, isForDisplay) { return isForDisplay ? this.utils.stringifyForDisplay(val) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { if (opDef.sqlOp == "LIKE" || opDef.sqlOp == "NOT LIKE") { return this.utils.SqlString.escapeLike(val, op != "starts_with", op != "ends_with", sqlDialect); } else { return this.utils.SqlString.escape(val); } }, spelFormatValue: function (val) { return this.utils.spelEscape(val); }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), fullWidth: true, }, number: { type: "number", jsType: "number", valueSrc: "value", valueLabel: "Number", valuePlaceholder: "Enter number", valueLabels: [ { label: "Number from", placeholder: "Enter number from" }, { label: "Number to", placeholder: "Enter number to" }, ], formatValue: function (val, fieldDef, wgtDef, isForDisplay) { return isForDisplay ? this.utils.stringifyForDisplay(val) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { return this.utils.SqlString.escape(val); }, spelFormatValue: function (val, fieldDef, wgtDef) { const isFloat = wgtDef.step && !Number.isInteger(wgtDef.step); return this.utils.spelEscape(val, isFloat); }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, price: { type: "number", jsType: "number", valueSrc: "value", valueLabel: "Price", valueLabels: [ { label: "Price from", placeholder: "Enter price from" }, { label: "Price to", placeholder: "Enter price to" }, ], formatValue: function (val, fieldDef, wgtDef, isForDisplay) { return isForDisplay ? this.utils.stringifyForDisplay(val) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef) { return this.utils.SqlString.escape(val); }, spelFormatValue: function (val, fieldDef, wgtDef) { const isFloat = wgtDef.step && !Number.isInteger(wgtDef.step); return this.utils.spelEscape(val, isFloat); }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, slider: { type: "number", jsType: "number", valueSrc: "value", valueLabel: "Number", valuePlaceholder: "Enter number or move slider", formatValue: function (val, fieldDef, wgtDef, isForDisplay) { return isForDisplay ? this.utils.stringifyForDisplay(val) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { return this.utils.SqlString.escape(val); }, spelFormatValue: function (val) { return this.utils.spelEscape(val); }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, select: { type: "select", jsType: "string", valueSrc: "value", valueLabel: "Value", valuePlaceholder: "Select value", formatValue: function (val, fieldDef, wgtDef, isForDisplay) { let valLabel = this.utils.getTitleInListValues(fieldDef.fieldSettings.listValues || fieldDef.asyncListValues, val); return isForDisplay ? this.utils.stringifyForDisplay(valLabel) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { return this.utils.SqlString.escape(val); }, spelFormatValue: function (val) { return this.utils.spelEscape(val); }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, multiselect: { type: "multiselect", jsType: "array", valueSrc: "value", valueLabel: "Values", valuePlaceholder: "Select values", formatValue: function (vals, fieldDef, wgtDef, isForDisplay) { let valsLabels = vals.map(v => this.utils.getTitleInListValues(fieldDef.fieldSettings.listValues || fieldDef.asyncListValues, v)); return isForDisplay ? valsLabels.map(this.utils.stringifyForDisplay) : vals.map(JSON.stringify); }, sqlFormatValue: function (vals, fieldDef, wgtDef, op, opDef, _, sqlDialect) { return vals.map(v => this.utils.SqlString.escape(v)); }, spelFormatValue: function (vals, fieldDef, wgtDef, op, opDef) { const isCallable = opDef && opDef.spelOp && opDef.spelOp.startsWith("${1}"); let res = this.utils.spelEscape(vals); // inline list if (isCallable) { // `{1,2}.contains(1)` NOT works // `{1,2}.?[true].contains(1)` works res = this.utils.spelFixList(res); } return res; }, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, date: { type: "date", jsType: "string", valueSrc: "value", dateFormat: "DD.MM.YYYY", valueFormat: "YYYY-MM-DD", valueLabel: "Date", valuePlaceholder: "Enter date", valueLabels: [ { label: "Date from", placeholder: "Enter date from" }, { label: "Date to", placeholder: "Enter date to" }, ], formatValue: function (val, fieldDef, wgtDef, isForDisplay) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); return isForDisplay ? dateVal.format(wgtDef.dateFormat) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); return this.utils.SqlString.escape(dateVal.format("YYYY-MM-DD")); }, spelFormatValue: function (val, fieldDef, wgtDef, op, opDef) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); const v = dateVal.format("YYYY-MM-DD"); const fmt = "yyyy-MM-dd"; //return `new java.text.SimpleDateFormat('${fmt}').parse('${v}')`; return `T(java.time.LocalDate).parse('${v}', T(java.time.format.DateTimeFormatter).ofPattern('${fmt}'))`; }, spelImportFuncs: [ //"new java.text.SimpleDateFormat(${fmt}).parse(${v})", { obj: { cls: ["java", "time", "LocalDate"], }, methodName: "parse", args: [ {var: "v"}, { obj: { cls: ["java", "time", "format", "DateTimeFormatter"], }, methodName: "ofPattern", args: [ {var: "fmt"} ] }, ], } ], spelImportValue: function (val, wgtDef, args) { if (!wgtDef) return [undefined, "No widget def to get value format"]; if (args?.fmt?.value?.includes?.(" ") || args.fmt?.value?.toLowerCase?.().includes("hh:mm")) return [undefined, `Invalid date format ${JSON.stringify(args.fmt)}`]; const dateVal = this.utils.moment(val.value, this.utils.moment.ISO_8601); if (dateVal.isValid()) { return [dateVal.format(wgtDef?.valueFormat), []]; } else { return [undefined, "Invalid date"]; } }, jsonLogic: function (val, fieldDef, wgtDef) { // tip: we use UTC to return same result as new Date(val) // new Date("2000-01-01") is now the same as new Date("2000-01-01 00:00:00") (first one in UTC) return this.utils.moment.utc(val, wgtDef.valueFormat).toDate(); }, toJS: function (val, fieldSettings) { const dateVal = this.utils.moment(val, fieldSettings.valueFormat); return dateVal.isValid() ? dateVal.toDate() : undefined; }, mongoFormatValue: function (val, fieldDef, wgtDef) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); if (dateVal.isValid()) { return { "$dateFromString": { "dateString": dateVal.format("YYYY-MM-DD"), "format": "%Y-%m-%d" } }; } return undefined; } }, time: { type: "time", jsType: "string", valueSrc: "value", timeFormat: "HH:mm", valueFormat: "HH:mm:ss", use12Hours: false, valueLabel: "Time", valuePlaceholder: "Enter time", valueLabels: [ { label: "Time from", placeholder: "Enter time from" }, { label: "Time to", placeholder: "Enter time to" }, ], formatValue: function (val, fieldDef, wgtDef, isForDisplay) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); return isForDisplay ? dateVal.format(wgtDef.timeFormat) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); return this.utils.SqlString.escape(dateVal.format("HH:mm:ss")); }, spelFormatValue: function (val, fieldDef, wgtDef, op, opDef) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); const fmt = "HH:mm:ss"; const v = dateVal.format("HH:mm:ss"); return `T(java.time.LocalTime).parse('${v}')`; //return `new java.text.SimpleDateFormat('${fmt}').parse('${v}')`; }, spelImportFuncs: [ "T(java.time.LocalTime).parse(${v})", //"new java.text.SimpleDateFormat(${fmt}).parse(${v})" ], spelImportValue: function (val, wgtDef, args) { if (!wgtDef) return [undefined, "No widget def to get value format"]; if (args?.fmt && (!args.fmt?.value?.toLowerCase?.().includes("hh:mm") || args.fmt?.value?.includes(" "))) return [undefined, `Invalid time format ${JSON.stringify(args.fmt)}`]; const dateVal = this.utils.moment(val.value, "HH:mm:ss"); if (dateVal.isValid()) { return [dateVal.format(wgtDef?.valueFormat), []]; } else { return [undefined, "Invalid date"]; } }, jsonLogic: function (val, fieldDef, wgtDef) { // return seconds of day const dateVal = this.utils.moment(val, wgtDef.valueFormat); return dateVal.get("hour") * 60 * 60 + dateVal.get("minute") * 60 + dateVal.get("second"); }, toJS: function (val, fieldSettings) { // return seconds of day const dateVal = this.utils.moment(val, fieldSettings.valueFormat); return dateVal.isValid() ? dateVal.get("hour") * 60 * 60 + dateVal.get("minute") * 60 + dateVal.get("second") : undefined; }, mongoFormatValue: function (val, fieldDef, wgtDef) { // return seconds of day const dateVal = this.utils.moment(val, wgtDef.valueFormat); return dateVal.get("hour") * 60 * 60 + dateVal.get("minute") * 60 + dateVal.get("second"); }, elasticSearchFormatValue: function elasticSearchFormatValue(queryType, value, operator, fieldName) { return { script: { script: { source: "doc[".concat(fieldName, "][0].getHour() >== params.min && doc[").concat(fieldName, "][0].getHour() <== params.max"), params: { min: value[0], max: value[1] } } } }; }, }, datetime: { type: "datetime", jsType: "string", valueSrc: "value", timeFormat: "HH:mm", dateFormat: "DD.MM.YYYY", valueFormat: "YYYY-MM-DD HH:mm:ss", use12Hours: false, valueLabel: "Datetime", valuePlaceholder: "Enter datetime", valueLabels: [ { label: "Datetime from", placeholder: "Enter datetime from" }, { label: "Datetime to", placeholder: "Enter datetime to" }, ], formatValue: function (val, fieldDef, wgtDef, isForDisplay) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); return isForDisplay ? dateVal.format(wgtDef.dateFormat + " " + wgtDef.timeFormat) : JSON.stringify(val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); return this.utils.SqlString.escape(dateVal.toDate()); }, spelFormatValue: function (val, fieldDef, wgtDef, op, opDef) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); const v = dateVal.format("YYYY-MM-DD HH:mm:ss"); const fmt = "yyyy-MM-dd HH:mm:ss"; //return `new java.text.SimpleDateFormat('${fmt}').parse('${v}')`; return `T(java.time.LocalDateTime).parse('${v}', T(java.time.format.DateTimeFormatter).ofPattern('${fmt}'))`; }, spelImportFuncs: [ //"new java.text.SimpleDateFormat(${fmt}).parse(${v})", { obj: { cls: ["java", "time", "LocalDateTime"], }, methodName: "parse", args: [ {var: "v"}, { obj: { cls: ["java", "time", "format", "DateTimeFormatter"], }, methodName: "ofPattern", args: [ {var: "fmt"} ] }, ], } ], spelImportValue: function (val, wgtDef, args) { if (!wgtDef) return [undefined, "No widget def to get value format"]; if (!args?.fmt?.value?.includes?.(" ")) return [undefined, `Invalid datetime format ${JSON.stringify(args.fmt)}`]; const dateVal = this.utils.moment(val.value, this.utils.moment.ISO_8601); if (dateVal.isValid()) { return [dateVal.format(wgtDef?.valueFormat), []]; } else { return [undefined, "Invalid date"]; } }, // Moved to `sqlImportDate` in `packages/sql/modules/import/conv` // sqlImport: function (sqlObj, wgtDef, sqlDialect) { // if (["TO_DATE"].includes(sqlObj?.func) && sqlObj?.children?.length >= 1) { // const [valArg, patternArg] = sqlObj.children; // if (valArg?.valueType == "single_quote_string") { // // tip: moment doesn't support SQL date format, so ignore patternArg // const dateVal = this.utils.moment(valArg.value); // if (dateVal.isValid()) { // return { // value: dateVal.format(wgtDef?.valueFormat), // }; // } else { // return { // value: null, // error: "Invalid date", // }; // } // } // } // }, jsonLogic: function (val, fieldDef, wgtDef) { return this.utils.moment(val, wgtDef.valueFormat).toDate(); }, // Example of importing and exporting to epoch timestamp (in ms) for JsonLogic: // jsonLogicImport: function(timestamp, wgtDef) { // const momentVal = this.utils.moment(timestamp, "x"); // return momentVal.isValid() ? momentVal.toDate() : undefined; // }, // jsonLogic: function (val, fieldDef, wgtDef) { // return this.utils.moment(val, wgtDef.valueFormat).format("x"); // }, toJS: function (val, fieldSettings) { const dateVal = this.utils.moment(val, fieldSettings.valueFormat); return dateVal.isValid() ? dateVal.toDate() : undefined; }, // todo: $toDate (works onliny in $expr) // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ mongoFormatValue: function (val, fieldDef, wgtDef) { const dateVal = this.utils.moment(val, wgtDef.valueFormat); if (dateVal.isValid()) { return { "$dateFromString": { "dateString": dateVal.format("YYYY-MM-DD HH:mm:ss"), "format": "%Y-%m-%d %H:%M:%S" } }; } } }, boolean: { type: "boolean", jsType: "boolean", valueSrc: "value", labelYes: "Yes", labelNo: "No", formatValue: (val, fieldDef, wgtDef, isForDisplay) => { return isForDisplay ? (val ? "Yes" : "No") : JSON.stringify(!!val); }, sqlFormatValue: function (val, fieldDef, wgtDef, op, opDef, _, sqlDialect) { return this.utils.SqlString.escape(val); }, spelFormatValue: function (val, fieldDef, wgtDef, op, opDef) { return this.utils.spelEscape(val); }, defaultValue: false, toJS: (val, fieldSettings) => (val), mongoFormatValue: (val, fieldDef, wgtDef) => (val), }, field: { valueSrc: "field", formatValue: (val, fieldDef, wgtDef, isForDisplay, op, opDef, rightFieldDef) => { return isForDisplay ? (rightFieldDef.label || val) : val; }, sqlFormatValue: (val, fieldDef, wgtDef, op, opDef, rightFieldDef, sqlDialect) => { return val; }, spelFormatValue: (val, fieldDef, wgtDef, op, opDef) => { return val; }, valueLabel: "Field to compare", valuePlaceholder: "Select field to compare", }, func: { valueSrc: "func", valueLabel: "Function", valuePlaceholder: "Select function", }, /** * @deprecated */ case_value: { valueSrc: "value", type: "case_value", spelFormatValue: function (val) { return this.utils.spelEscape(val === "" ? null : val); }, spelImportValue: (val) => { return [val.value, []]; }, jsonLogic: function (val) { return val === "" ? null : val; }, } }; //---------------------------- types const types = { text: { defaultOperator: "equal", mainWidget: "text", widgets: { text: { operators: [ "equal", "not_equal", "like", "not_like", "starts_with", "ends_with", "proximity", "is_empty", "is_not_empty", "is_null", "is_not_null", ], widgetProps: {}, opProps: {}, }, textarea: { operators: [ "equal", "not_equal", "like", "not_like", "starts_with", "ends_with", "is_empty", "is_not_empty", "is_null", "is_not_null", ], widgetProps: {}, opProps: {}, }, field: { operators: [ //unary ops (like `is_empty`) will be excluded anyway, see getWidgetsForFieldOp() "equal", "not_equal", "proximity", //can exclude if you want ], } }, }, number: { defaultOperator: "equal", mainWidget: "number", widgets: { number: { operators: [ "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", "between", "not_between", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ], }, price: { operators: [ "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", "between", "not_between", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ], }, slider: { operators: [ "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", // "is_empty", // "is_not_empty", "is_null", "is_not_null" ], }, }, }, date: { defaultOperator: "equal", widgets: { date: { operators: [ "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", "between", "not_between", // "is_empty", // "is_not_empty", "is_null", "is_not_null" ] } }, }, time: { defaultOperator: "equal", widgets: { time: { operators: [ "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", "between", "not_between", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ] } }, }, datetime: { defaultOperator: "equal", widgets: { datetime: { operators: [ "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", "between", "not_between", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ], } }, }, select: { mainWidget: "select", defaultOperator: "select_equals", widgets: { select: { operators: [ "select_equals", "select_not_equals", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ], }, multiselect: { operators: [ "select_any_in", "select_not_any_in", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ], }, }, }, multiselect: { defaultOperator: "multiselect_equals", widgets: { multiselect: { operators: [ "multiselect_contains", "multiselect_not_contains", "multiselect_equals", "multiselect_not_equals", // "is_empty", // "is_not_empty", "is_null", "is_not_null", ] } }, }, boolean: { defaultOperator: "equal", widgets: { boolean: { operators: [ "equal", "not_equal", "is_null", "is_not_null", ], widgetProps: { //you can enable this if you don't use fields as value sources // hideOperator: true, // operatorInlineLabel: "is", } }, field: { operators: [ "equal", "not_equal", ], } }, }, "!group": { defaultOperator: "some", mainWidget: "number", widgets: { number: { widgetProps: { min: 0 }, operators: [ // w/o operand "some", "all", "none", // w/ operand - count "equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal", "between", "not_between", ], opProps: { equal: { label: "Count ==" }, not_equal: { label: "Count !=" }, less: { label: "Count <" }, less_or_equal: { label: "Count <=" }, greater: { label: "Count >" }, greater_or_equal: { label: "Count >=" }, between: { label: "Count between" }, not_between: { label: "Count not between" } } } } }, /** * @deprecated */ "case_value": { mainWidget: "case_value", widgets: { case_value: { widgetProps: {}, } } }, }; //---------------------------- settings const settings = { ...defaultSettings, convertableWidgets: { "number": ["slider", "rangeslider", "price"], "slider": ["number", "rangeslider", "price"], "rangeslider": ["number", "slider", "price"], "price": ["number", "slider", "rangeslider"], "text": ["textarea"], "textarea": ["text"] }, formatSpelField: function (field, parentField, parts, partsExt, fieldDefinition, config) { let fieldName = partsExt.map(({key, parent, fieldSeparator: sep}, ind) => { if (ind == 0) { if (parent == "[map]") return `#this[${this.utils.spelEscape(key)}]`; else if