@react-query-builder-express/core
Version:
User-friendly query builder for React. Core
391 lines (349 loc) • 13.7 kB
JavaScript
import uuid from "./uuid";
import mergeWith from "lodash/mergeWith";
import {settings as defaultSettings} from "../config/default";
import moment from "moment";
import {mergeArraysSmart, logger, deepFreeze, mergeCustomizerNoArrays, shallowCopy, omit} from "./stuff";
import clone from "clone";
import { compileConfig } from "./configSerialize";
import { getFieldRawConfig } from "./configUtils";
import { findExtendedConfigInAllMemos, getCommonMemo } from "./configMemo";
export const extendConfig = (config, configId, canCompile = true) => {
//operators, defaultOperator - merge
//widgetProps (including valueLabel, valuePlaceholder, hideOperator, operatorInlineLabel) - concrete by widget
canCompile = canCompile && config.settings.useConfigCompress;
// Already extended?
if (config.__configId) {
return config;
}
// Try to take from memo (cache)
const cachedExtConfig = findExtendedConfigInAllMemos(config, canCompile);
if (cachedExtConfig) {
return cachedExtConfig;
}
const origConfig = config;
// Clone (and compile if need)
if (canCompile) {
if (config.__compliled) {
// already compiled
config = clone(config);
} else {
// will be cloned and compiled
config = compileConfig(config);
}
} else {
config = clone(config);
}
config.settings = mergeWith({}, defaultSettings, config.settings, mergeCustomizerNoArrays);
config.__fieldsCntByType = {};
config.__funcsCntByType = {};
config.__fieldNames = {};
extendTypesConfig(config.types, config);
extendFieldsConfig(config.fields, config);
extendFuncsConfig(config.funcs, config);
const { caseValueField } = config.settings;
if (caseValueField) {
extendFieldConfig(caseValueField, config, [], false, true);
}
const momentLocale = config.settings.locale.moment;
if (momentLocale) {
moment.locale(momentLocale);
}
Object.defineProperty(config, "__configId", {
enumerable: false,
writable: false,
value: configId || uuid()
});
config.__cache = {};
deepFreeze(config);
// Save to memo (cache)
const memo = getCommonMemo();
memo.storeConfigPair(origConfig, config);
return config;
};
function extendTypesConfig(typesConfig, config) {
for (let type in typesConfig) {
let typeConfig = typesConfig[type];
extendTypeConfig(type, typeConfig, config);
}
}
function extendTypeConfig(type, typeConfig, config) {
let operators = null, defaultOperator = null;
typeConfig.mainWidget = typeConfig.mainWidget || Object.keys(typeConfig.widgets).filter(w => w != "field" && w != "func")[0];
const excludeOperators = typeConfig.excludeOperators || [];
for (let widget in typeConfig.widgets) {
const typeWidgetConfig = typeConfig.widgets[widget];
const defOp = typeWidgetConfig.defaultOperator;
if (typeWidgetConfig.operators) {
const typeWidgetOperators = typeWidgetConfig.operators.filter(op => !excludeOperators.includes(op));
operators = mergeArraysSmart(operators, typeWidgetOperators);
}
if (defOp && !excludeOperators.includes(defOp)) {
if (!defaultOperator || widget === typeConfig.mainWidget) {
defaultOperator = defOp;
}
}
if (widget == typeConfig.mainWidget) {
typeWidgetConfig.widgetProps = {
...(typeConfig.mainWidgetProps || {}),
...(typeWidgetConfig.widgetProps || {}),
};
}
typeConfig.widgets[widget] = typeWidgetConfig;
}
if (!typeConfig.valueSources)
typeConfig.valueSources = Object.keys(config.settings.valueSourcesInfo);
for (let valueSrc of typeConfig.valueSources) {
if (valueSrc != "value" && !typeConfig.widgets[valueSrc]) {
typeConfig.widgets[valueSrc] = {
};
}
}
if (!typeConfig.operators && operators)
typeConfig.operators = Array.from(new Set(operators)); //unique
if (!typeConfig.defaultOperator && defaultOperator)
typeConfig.defaultOperator = defaultOperator;
}
function extendFieldsConfig(subconfig, config, path = []) {
for (let field in subconfig) {
const fieldPathArr = [...path, field];
extendFieldConfig(subconfig[field], config, fieldPathArr);
if (subconfig[field].subfields) {
extendFieldsConfig(subconfig[field].subfields, config, fieldPathArr);
}
}
}
function extendFuncsConfig(subconfig, config, path = []) {
if (!subconfig) return;
const fieldSeparator = config?.settings?.fieldSeparator || ".";
for (let funcKey in subconfig) {
const funcPathArr = [...path, funcKey];
const funcPathStr = funcPathArr.join(fieldSeparator);
const funcDef = subconfig[funcKey];
if (funcDef.returnType) {
funcDef.type = funcDef.returnType;
if (!config.__funcsCntByType[funcDef.returnType])
config.__funcsCntByType[funcDef.returnType] = 0;
config.__funcsCntByType[funcDef.returnType]++;
}
extendFieldConfig(funcDef, config, funcPathArr, false);
if (funcDef.args) {
for (let argKey in funcDef.args) {
extendFieldConfig(funcDef.args[argKey], config, [...funcPathArr, argKey], true);
}
// isOptional can be only in the end
const argKeys = Object.keys(funcDef.args);
let tmpIsOptional = true;
for (const argKey of argKeys.reverse()) {
const argDef = funcDef.args[argKey];
if (!tmpIsOptional && argDef.isOptional) {
logger.info(`Arg ${argKey} for func ${funcPathStr} can't be optional`);
delete argDef.isOptional;
}
if (!argDef.isOptional)
tmpIsOptional = false;
}
}
if (funcDef.subfields) {
extendFuncsConfig(funcDef.subfields, config, [...path, funcKey]);
}
}
}
function normalizeFieldSettings(fieldConfig, config, type) {
const keysToPutInFieldSettings = ["listValues", "treeValues", "allowCustomValues", "validateValue"];
for (const k of keysToPutInFieldSettings) {
if (fieldConfig[k]) {
if (!fieldConfig.fieldSettings)
fieldConfig.fieldSettings = {};
fieldConfig.fieldSettings[k] = fieldConfig[k];
delete fieldConfig[k];
}
}
// normalize listValues
if (fieldConfig.fieldSettings?.listValues) {
if (config.settings.normalizeListValues) {
fieldConfig.fieldSettings.listValues = config.settings.normalizeListValues.call(
config.ctx,
fieldConfig.fieldSettings.listValues, type, fieldConfig.fieldSettings
);
}
}
// same for treeValues
if (fieldConfig.fieldSettings?.treeValues) {
if (config.settings.normalizeListValues) {
fieldConfig.fieldSettings.treeValues = config.settings.normalizeListValues.call(
config.ctx,
fieldConfig.fieldSettings.treeValues, type, fieldConfig.fieldSettings
);
}
}
}
function extendFieldConfig(fieldConfig, config, path = [], isFuncArg = false, isCaseValue = false) {
let { showLabels, fieldSeparator } = config.settings;
fieldSeparator = fieldSeparator ?? ".";
const argKey = path[path.length - 1];
const funcKey = isFuncArg ? path.slice(0, path.length-1).join(fieldSeparator) : path.join(fieldSeparator);
const isFunc = !!fieldConfig.returnType;
const type = fieldConfig.type || fieldConfig.returnType;
// const isGroup = type === "!struct" || type === "!group";
const typeConfig = config.types[type];
const excludeOperatorsForField = fieldConfig.excludeOperators || [];
let operators = (fieldConfig.operators || typeConfig?.operators || []).filter(op => !excludeOperatorsForField.includes(op));
let defaultOperator = fieldConfig.defaultOperator || typeConfig?.defaultOperator;
if (excludeOperatorsForField.includes(defaultOperator))
defaultOperator = undefined;
const hasOwnDefaultOperator = !!defaultOperator && defaultOperator == fieldConfig.defaultOperator;
if (hasOwnDefaultOperator) {
fieldConfig.ownDefaultOperator = fieldConfig.defaultOperator;
}
if (!typeConfig) {
// console.warn(`No type config for ${type}`);
fieldConfig.disabled = true;
return;
}
if (!isFuncArg && !isFunc && !isCaseValue) {
if (!config.__fieldsCntByType[type])
config.__fieldsCntByType[type] = 0;
config.__fieldsCntByType[type]++;
}
if (isFuncArg) {
fieldConfig._isFuncArg = true;
fieldConfig._argKey = argKey;
fieldConfig._funcKey = funcKey;
}
if (isFunc) {
fieldConfig._isFunc = true;
fieldConfig._funcKey = funcKey;
}
if (isCaseValue) {
fieldConfig._isCaseValue = true;
}
normalizeFieldSettings(fieldConfig, config, type);
// copy from type to field
const excludeKeysFromType = ["widgets", "operators", "defaultOperator"];
Object.keys(typeConfig).filter((k) => !excludeKeysFromType.includes(k)).map((k) => {
if (!fieldConfig[k]) {
fieldConfig[k] = shallowCopy(typeConfig[k]);
}
});
// copy/merge widgets
let excludeOperatorsForType = (typeConfig.excludeOperators || []);
if (fieldConfig.operators) {
// `operators` from field can override `excludeOperators` from type, see `prox1` at examples
excludeOperatorsForType = excludeOperatorsForType.filter(op => !fieldConfig.operators.includes(op));
}
if (!fieldConfig.widgets)
fieldConfig.widgets = {};
for (let widget in typeConfig.widgets) {
let fieldWidgetConfig = { ...(fieldConfig.widgets[widget] || {}) };
const typeWidgetConfig = typeConfig.widgets[widget] || {};
// merge operators, defaultOperator
if (!isFuncArg) { // tip: operators are not used for func args
const defOp = fieldWidgetConfig.defaultOperator;
const excludeOperators = [...excludeOperatorsForField, ...excludeOperatorsForType];
const shouldIncludeOperators = fieldConfig.preferWidgets
&& (widget === "field" || fieldConfig.preferWidgets.includes(widget))
|| excludeOperators.length > 0;
if (fieldWidgetConfig.operators) {
const addOperators = fieldWidgetConfig.operators.filter(op => !excludeOperators.includes(op));
fieldWidgetConfig.operators = addOperators;
// operators = [...(operators || []), ...addOperators];
operators = mergeArraysSmart(operators, addOperators);
} else if (shouldIncludeOperators && typeWidgetConfig.operators) {
const addOperators = typeWidgetConfig.operators.filter(op => !excludeOperators.includes(op));
fieldWidgetConfig.operators = addOperators;
// operators = [...(operators || []), ...addOperators];
operators = mergeArraysSmart(operators, addOperators);
}
if (defOp && !excludeOperators.includes(defOp)) {
if (!defaultOperator || !hasOwnDefaultOperator && widget === fieldConfig.mainWidget) {
// tip: defOp can overwrite default operator from type config
defaultOperator = defOp;
}
}
}
// merge widgetProps
if (widget === fieldConfig.mainWidget) {
fieldWidgetConfig.widgetProps = {
...(typeWidgetConfig.widgetProps || {}),
...(fieldConfig.mainWidgetProps || {}),
...(fieldWidgetConfig.widgetProps || {}),
};
} else {
fieldWidgetConfig.widgetProps = {
...(typeWidgetConfig.widgetProps || {}),
...(fieldWidgetConfig.widgetProps || {}),
};
}
// merge opProps
const opKeys = Array.from(new Set([
...Object.keys(typeWidgetConfig.opProps || {}),
...Object.keys(fieldWidgetConfig.opProps || {}),
]));
if (opKeys.length) {
const opProps = {};
for (let op of opKeys) {
opProps[op] = {
...(typeWidgetConfig.opProps?.[op] || {}),
...(fieldWidgetConfig.opProps?.[op] || {}),
};
}
fieldWidgetConfig.opProps = opProps;
}
// label for func arg
let { valueLabel, valuePlaceholder } = fieldWidgetConfig;
if (isFuncArg) {
if (!valueLabel)
fieldWidgetConfig.valueLabel = fieldConfig.label || argKey;
if (!valuePlaceholder && !showLabels)
fieldWidgetConfig.valuePlaceholder = fieldConfig.label || argKey;
}
// copy other widget configs from type to field
fieldWidgetConfig = {
...typeWidgetConfig,
...fieldWidgetConfig,
};
fieldConfig.widgets[widget] = fieldWidgetConfig;
}
if (!isFuncArg) { // tip: operators are not used for func args
if (!fieldConfig.operators) {
fieldConfig.operators = Array.from(new Set(operators)); // unique
}
if (!fieldConfig.defaultOperator) {
fieldConfig.defaultOperator = defaultOperator;
}
}
if (!isFuncArg && !isFunc && !isCaseValue) {
const { fieldName, inGroup } = computeFieldName(config, path);
if (fieldName) {
fieldConfig.fieldName = fieldName;
if (!config.__fieldNames[fieldName])
config.__fieldNames[fieldName] = [];
config.__fieldNames[fieldName].push({fullPath: path, inGroup});
}
}
}
function computeFieldName(config, path) {
if (!path)
return {};
const fieldSeparator = config.settings.fieldSeparator;
const {computedPath, computed, inGroup} = [...path].reduce(({computedPath, computed, inGroup}, f, i, arr) => {
const fullPath = [...arr.slice(0, i), f];
const fConfig = getFieldRawConfig(config, fullPath);
if (fConfig?.type === "!group" && i < arr.length-1) {
// don't include group in final field name
inGroup = fullPath.join(fieldSeparator);
computedPath = [];
} else if (fConfig?.fieldName) {
// tip: fieldName overrides path !
computed = true;
computedPath = [fConfig.fieldName];
} else {
computedPath = [...computedPath, f];
}
return {computedPath, computed, inGroup};
}, {computedPath: [], computed: false, inGroup: undefined});
return computed ? {
fieldName: computedPath.join(fieldSeparator),
inGroup,
} : {};
}