@react-awesome-query-builder/sql
Version:
User-friendly query builder for React. SQL support
489 lines (451 loc) • 16.8 kB
text/typescript
/* eslint-disable @typescript-eslint/no-redundant-type-constituents, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
import type { Conv, FuncArgsObj, Meta, OperatorObj, FuncWithArgsObj, OutLogic, ValueObj } from "./types";
import {
Config, JsonRule, JsonGroup, JsonSwitchGroup, JsonCaseGroup, CaseGroupProperties, FieldConfigExt,
BaseWidget, Utils, ValueSource, RuleProperties, SqlImportFunc, JsonAnyRule,
FuncArg,
Func,
FuncValue,
Field,
JsonRuleGroup,
} from "@react-awesome-query-builder/core";
import { getLogicDescr } from "./ast";
import { SqlPrimitiveTypes } from "./conv";
import { ValueExpr } from "node-sql-parser";
export const convertToTree = (
logic: OutLogic | undefined, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic, returnGroup = false
): JsonRule | JsonGroup | JsonRuleGroup | JsonSwitchGroup | JsonCaseGroup | undefined => {
if (!logic) return undefined;
let res;
if (logic.operator) {
res = convertOp(logic, conv, config, meta, parentLogic);
} else if (logic.conj) {
res = convertConj(logic, conv, config, meta, parentLogic);
} else if (logic.ternaryChildren) {
res = convertTernary(logic, conv, config, meta, parentLogic);
} else if (logic.func) {
// try to use `sqlImport` in operator definitions
res = convertOp(logic, conv, config, meta, parentLogic);
} else {
meta.errors.push(`Unexpected logic: ${getLogicDescr(logic)}`);
}
if (res?.type === "group") {
res = groupToMaybeRuleGroup(res, config);
}
if (returnGroup && res && res.type != "group" && res.type != "switch_group") {
res = wrapInDefaultConj(res, config, logic.not);
}
return res;
};
const convertTernary = (logic: OutLogic, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): JsonSwitchGroup => {
const { ternaryChildren } = logic;
const cases = (ternaryChildren ?? []).map(([cond, val]) => {
return buildCase(cond, val, conv, config, meta, logic);
}) as JsonCaseGroup[];
return {
type: "switch_group",
children1: cases,
};
};
const buildCase = (cond: OutLogic | undefined, val: OutLogic, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): JsonCaseGroup | undefined => {
const valProperties = buildCaseValProperties(config, meta, conv, val, parentLogic);
let caseI: JsonCaseGroup | undefined;
if (cond) {
caseI = convertToTree(cond, conv, config, meta, parentLogic, true) as JsonCaseGroup;
if (caseI && caseI.type) {
caseI.type = "case_group";
} else {
meta.errors.push(`Unexpected case: ${JSON.stringify(caseI)}`);
caseI = undefined;
}
} else {
caseI = {
type: "case_group",
properties: {}
};
}
if (caseI) {
caseI.properties = {
...caseI.properties,
...valProperties,
};
}
return caseI;
};
const buildCaseValProperties = (config: Config, meta: Meta, conv: Conv, val: OutLogic, parentLogic?: OutLogic): CaseGroupProperties => {
const caseValueFieldConfig = Utils.ConfigUtils.getFieldConfig(config, "!case_value") as FieldConfigExt;
const widget = caseValueFieldConfig?.mainWidget!;
const widgetConfig = config.widgets[widget] as BaseWidget;
const convVal = convertArg(val, conv, config, meta, parentLogic)!;
if (convVal && convVal.valueSrc === "value") {
// @ts-ignore
convVal.valueType = (widgetConfig.type || caseValueFieldConfig.type || convVal.valueType);
}
const valProperties = {
value: [convVal.value],
valueSrc: [convVal.valueSrc as ValueSource],
valueType: [convVal.valueType! as string],
field: "!case_value",
};
return valProperties;
};
const convertConj = (logic: OutLogic, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): JsonGroup => {
const { conj, children } = logic;
const conjunction = conv.conjunctions[conj!];
const convChildren = (children || []).map(a => convertToTree(a, conv, config, meta, logic)).filter(c => !!c) as JsonGroup[];
const res: JsonGroup = {
type: "group",
properties: {
conjunction,
not: logic.not,
},
children1: convChildren,
};
return res;
};
const getCommonGroupField = (fields: string[], config: Config): string | undefined => {
if (fields.length > 0) {
const closestGroupFields = fields.map(f => {
const paths = Utils.ConfigUtils.getFieldParts(f, config);
const groupFields = paths.filter(path => (Utils.ConfigUtils.getFieldConfig(config, path) as Field)?.type === "!group");
const closestGroupField = groupFields.reverse()?.[0];
return closestGroupField;
}).filter(gf => !!gf);
const isSameGroupField = Array.from(new Set(closestGroupFields)).length === 1;
const allFieldsAreInsideGroup = closestGroupFields.length === fields.length;
if (allFieldsAreInsideGroup && isSameGroupField) {
return closestGroupFields[0];
}
}
return undefined;
};
const groupToMaybeRuleGroup = (grp: JsonGroup, config: Config): JsonRuleGroup | JsonGroup => {
const fields = (grp.children1 ?? [])
.filter(ch => ch.type === "rule" || ch.type === "rule_group")
.map(rule => (rule as JsonRule | JsonRuleGroup).properties?.field)
.filter(f => !!f) as string[];
if (fields?.length === grp.children1?.length) {
const commonGroupField = getCommonGroupField(fields, config);
if (commonGroupField) {
const rgr = grp as any as JsonRuleGroup;
rgr.type = "rule_group";
rgr.properties!.field = commonGroupField;
(rgr.properties as any).fieldSrc = "field";
return rgr;
}
}
return grp;
};
const convertOp = (logic: OutLogic, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): JsonRule | undefined => {
let opKeys = conv.operators[logic.operator!];
let opKey = opKeys?.[0];
let convChildren;
let operatorOptions;
// tip: Even if opKeys.length === 1, still try to use `convertOpFunc` to let `sqlImport` be applied first (eg. `not_like` op)
const convFuncOp = convertOpFunc(logic, conv, config, meta, parentLogic)!;
if (convFuncOp) {
opKey = convFuncOp.operator!;
convChildren = convFuncOp.children;
operatorOptions = convFuncOp.operatorOptions;
} else if (logic.operator) {
convChildren = (logic.children || []).map(a => convertArg(a, conv, config, meta, logic));
const isMultiselect = convChildren.filter(ch => ch?.valueType === "multiselect").length > 0;
const isSelect = convChildren.filter(ch => ch?.valueType === "select").length > 0;
if (opKeys?.length > 1) {
if (isMultiselect) {
opKeys = opKeys.filter(op => !["equal", "not_equal", "select_equals", "select_not_equals"].includes(op));
} else if (isSelect) {
opKeys = opKeys.filter(op => !["equal", "not_equal"].includes(op));
}
opKey = opKeys?.[0];
}
if (!opKeys?.length) {
meta.errors.push(`Can't convert ${getLogicDescr(logic)}`);
return undefined;
} else if (opKeys.length > 1 && !["=", "!=", "<>"].includes(logic.operator!)) {
meta.warnings.push(`SQL operator "${logic.operator}" can be converted to several operators: ${opKeys.join(", ")}`);
}
} else {
meta.errors.push(`Can't convert ${getLogicDescr(logic)}`);
return undefined;
}
const [left, ...right] = (convChildren || []).filter(c => !!c);
const properties: RuleProperties = {
operator: opKey,
value: [],
valueSrc: [],
valueType: [],
valueError: [],
field: undefined,
};
if (operatorOptions) {
properties.operatorOptions = operatorOptions;
}
if (left?.valueSrc === "field") {
properties.field = left.value;
properties.fieldSrc = "field";
} else if (left?.valueSrc === "func") {
properties.field = left.value;
properties.fieldSrc = left.valueSrc;
}
const opDef = Utils.ConfigUtils.getOperatorConfig(config, opKey, properties.field ?? undefined);
// const opCardinality = opDef?.cardinality;
const opValueTypes = opDef?.valueTypes;
right.forEach((v, i) => {
if (v) {
properties.valueSrc![i] = v?.valueSrc as ValueSource;
let valueType: string = v?.valueType;
let finalVal = v?.value;
if (valueType && opValueTypes && !opValueTypes.includes(valueType)) {
meta.warnings.push(`Operator "${opKey}" supports value types [${opValueTypes.join(", ")}] but got ${valueType}`);
}
if (opValueTypes?.length === 1) {
valueType = opValueTypes[0];
}
if (valueType && v?.valueSrc === "value") {
finalVal = checkSimpleValueType(finalVal, valueType);
}
properties.valueType![i] = valueType;
properties.value[i] = finalVal;
if (v?.valueError) {
properties.valueError![i] = v.valueError;
}
}
});
return {
type: "rule",
properties,
};
};
const convertArg = (logic: OutLogic | undefined, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): ValueObj | undefined => {
const { fieldSeparator } = config.settings;
if (logic?.valueType) {
const sqlType = logic?.valueType;
let valueType: string | undefined = SqlPrimitiveTypes[sqlType];
if (!valueType) {
meta.warnings.push(`Unexpected value type ${sqlType}`);
}
const value = logic.value; // todo: convert ?
if (valueType === "text") {
// fix issues with date/time values
valueType = undefined;
}
return {
valueSrc: "value",
valueType,
value,
};
} else if (logic?.field) {
let field = [logic.table, logic.field].filter(v => !!v).join(fieldSeparator);
for (const [fieldPath, fieldConfig, fieldKey] of Utils.ConfigUtils.iterateFields(config)) {
if (fieldConfig.tableName === logic.table && fieldKey === logic.field) {
field = fieldPath;
break;
}
}
const fieldConfig = Utils.ConfigUtils.getFieldConfig(config, field) as Field | undefined;
const valueType = fieldConfig?.type;
return {
valueSrc: "field",
valueType,
value: field,
};
} else if (logic?.children && logic._type === "expr_list") {
return {
valueSrc: "value",
valueType: "multiselect",
value: logic.values,
};
} else if (logic?.value) {
const value = logic.value; // todo: convert ?
return {
valueSrc: "value",
value,
};
} else {
const maybeFunc = convertValueFunc(logic, conv, config, meta, parentLogic) || convertFunc(logic, conv, config, meta, parentLogic);
if (maybeFunc) {
return maybeFunc;
}
}
meta.errors.push(`Unexpected arg: ${getLogicDescr(logic)}`);
return undefined;
};
const convertFuncArg = (logic: any, argConfig: FuncArg | undefined, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): ValueObj => {
if (typeof logic === "object" && logic !== null && !Array.isArray(logic)) {
return convertArg(logic as OutLogic, conv, config, meta, parentLogic)!;
}
return {
valueSrc: "value",
value: logic,
//valueType: // todo: see SpelPrimitiveTypes[spel.type] or argConfig
};
};
// `meta` should contain either `funcKey` or `opKey`
const useImportFunc = (
sqlImport: SqlImportFunc, logic: OutLogic | undefined, conv: Conv, config: Config, meta: Meta
): FuncWithArgsObj | OperatorObj | ValueObj | undefined => {
let parsed: Record<string, any> | undefined;
let widgetConfig: BaseWidget | undefined;
if (meta.widgetKey) {
widgetConfig = config.widgets[meta.widgetKey] as BaseWidget;
}
const args: any[] = [config.ctx, logic!, widgetConfig, config.settings.sqlDialect];
try {
parsed = (sqlImport as any).call(...args);
} catch(_e) {
// can't be parsed
}
if (parsed) {
const funcKey = parsed?.func as string ?? meta.funcKey;
const sqlFunc = logic?.func;
const parseKey = meta.opKey ?? meta.funcKey ?? meta.widgetKey ?? meta.outType ?? "?";
if (parsed?.children) {
return {
operator: parsed.operator ?? meta.opKey,
children: (parsed as OutLogic).children?.map(ch => convertArg(ch, conv, config, meta, logic)),
operatorOptions: parsed.operatorOptions,
} as OperatorObj;
} else if (parsed?.args) {
const funcConfig = Utils.ConfigUtils.getFuncConfig(config, funcKey);
const args: FuncArgsObj = {};
for (const argKey in parsed.args) {
const argLogic = parsed.args[argKey] as OutLogic;
const argConfig = funcConfig?.args[argKey];
args[argKey] = convertFuncArg(argLogic, argConfig, conv, config, meta, logic);
}
return {
func: funcKey,
funcConfig,
args,
};
} else if (Object.keys(parsed).includes("value")) {
const { value, error } = parsed;
if (error) {
meta.errors.push(`Error while parsing ${parseKey} with func ${sqlFunc ?? "?"}: ${error}`);
}
return {
value,
valueType: widgetConfig?.type,
valueSrc: "value",
valueError: error,
};
} else {
meta.errors.push(`Result of parsing as ${parseKey} should contain either 'children' or 'args' or 'value'`);
}
}
return undefined;
};
const convertOpFunc = (logic: OutLogic, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): OperatorObj | undefined => {
for (const opKey in conv.opFuncs) {
for (const f of conv.opFuncs[opKey]) {
const parsed = useImportFunc(f, logic, conv, config, {...meta, opKey: opKey, outType: "op"});
if (parsed) {
return parsed as OperatorObj;
}
}
}
return undefined;
};
const convertValueFunc = (logic: OutLogic | undefined, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic): ValueObj | undefined => {
for (const widgetKey in conv.valueFuncs) {
for (const f of conv.valueFuncs[widgetKey]) {
const parsed = useImportFunc(f, logic, conv, config, {...meta, outType: "value", widgetKey});
if (parsed) {
return parsed as ValueObj;
}
}
}
return undefined;
};
const convertFunc = (
logic: OutLogic | undefined, conv: Conv, config: Config, meta: Meta, parentLogic?: OutLogic
): ValueObj | undefined => {
let funcKey: string | undefined, argsObj: FuncArgsObj | undefined, funcConfig: Func | undefined | null;
for (const [f, fc] of Utils.ConfigUtils.iterateFuncs(config)) {
const { sqlFunc, sqlImport } = fc;
if (sqlImport) {
const parsed = useImportFunc(sqlImport, logic, conv, config, {...meta, funcKey: f, outType: "func"}) as FuncWithArgsObj;
if (parsed) {
funcKey = parsed.func;
funcConfig = parsed.funcConfig;
argsObj = parsed.args;
break;
}
}
if (sqlFunc && sqlFunc === logic?.func) {
funcKey = f;
funcConfig = Utils.ConfigUtils.getFuncConfig(config, funcKey);
argsObj = {};
let argIndex = 0;
for (const argKey in funcConfig!.args) {
const argLogic = logic.children?.[argIndex];
const argConfig = funcConfig?.args[argKey];
argsObj[argKey] = convertFuncArg(argLogic, argConfig, conv, config, meta, logic);
argIndex++;
}
break;
}
}
if (funcKey) {
const funcArgs: FuncArgsObj = {};
for (const argKey in funcConfig?.args) {
const argConfig = funcConfig.args[argKey];
let argVal = argsObj?.[argKey];
if (argVal === undefined) {
argVal = argConfig?.defaultValue;
if (argVal === undefined) {
if (argConfig?.isOptional) {
//ignore
} else {
meta.errors.push(`No value for arg ${argKey} of func ${funcKey}`);
return undefined;
}
} else {
argVal = {
value: argVal,
valueSrc: (argVal as any)["func"] ? "func" : "value",
valueType: argConfig.type,
};
}
}
if (argVal)
funcArgs[argKey] = argVal;
}
return {
valueSrc: "func",
value: {
func: funcKey,
args: funcArgs
} as FuncValue,
valueType: funcConfig!.returnType,
};
} else {
meta.errors.push(`Unexpected func: ${getLogicDescr(logic)}`);
return undefined;
}
};
const wrapInDefaultConj = (rule: JsonAnyRule, config: Config, not = false): JsonGroup => {
return {
type: "group",
id: Utils.uuid(),
children1: [
rule,
],
properties: {
conjunction: Utils.DefaultUtils.defaultConjunction(config),
not: not || false
}
};
};
const checkSimpleValueType = (val: any, valueType: string) => {
if (val != null && val?.func === undefined) {
if (valueType === "text") {
val = "" + val;
} else if (valueType === "multiselect" && val?.map === undefined) {
val = [val];
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return val;
};