@react-awesome-query-builder/core
Version:
User-friendly query builder for React. Core
582 lines (519 loc) • 16.4 kB
JavaScript
import Immutable, { Map } from "immutable";
import {default as uuid} from "./uuid";
import {default as clone} from "clone";
import {default as moment} from "moment";
export {uuid, clone, moment};
export const widgetDefKeysToOmit = [
"formatValue", "mongoFormatValue", "sqlFormatValue", "jsonLogic", "elasticSearchFormatValue", "spelFormatValue", "spelImportFuncs", "spelImportValue"
];
export const opDefKeysToOmit = [
"formatOp", "mongoFormatOp", "sqlFormatOp", "jsonLogic", "spelFormatOp"
];
export const isObject = (v) => {
return typeof v === "object" && v !== null && Object.prototype.toString.call(v) === "[object Object]";
};
// export const isObject = (v) => (typeof v == "object" && v !== null && !Array.isArray(v));
export const isObjectOrArray = (v) => (typeof v === "object" && v !== null);
export const typeOf = (v) => {
const t = (typeof v);
if (t && v !== null && Array.isArray(v))
return "array";
else
return t;
};
export const isTypeOf = (v, type) => {
if (typeOf(v) === type)
return true;
if (type === "number" && !isNaN(v))
return true; //can be casted
return false;
};
export const isTruthy = (value) => {
return !!value;
};
export const shallowCopy = (v) => {
if (typeof v === "object" && v !== null) {
if (Array.isArray(v)) {
return [...v];
} else if (isObject(v)) {
return {...v};
}
}
return v;
};
export const isSafeKeyForObject = (k) => {
return !["__proto__", "constructor", "prototype"].includes(k);
};
export const hasSafeOwnProperty = (obj, k) => {
return Object.prototype.hasOwnProperty.call(obj, k) && isSafeKeyForObject(k);
};
export const setIn = (obj, path, newValue, opts) => {
const defaultOpts = {
canCreate: false, canIgnore: false, canChangeType: false,
};
opts = { ...defaultOpts, ...(opts ?? {}) };
const { canCreate, canIgnore, canChangeType } = opts;
if (!Array.isArray(path)) {
throw new Error("path is not an array");
}
if (!path.length) {
throw new Error("path is empty");
}
const expectedObjType = typeof path[0] === "number" ? "array" : "object";
if (!isTypeOf(obj, expectedObjType)) {
throw new Error(`obj is not of type ${expectedObjType}`);
}
let newObj = shallowCopy(obj);
let target = newObj;
const pathToTarget = [...path];
const targetKey = pathToTarget.pop();
const goodPath = [];
for (const k of pathToTarget) {
if (!isSafeKeyForObject(k)) {
throw new Error(`path contains unsafe ${k}`);
}
const nextKey = path[goodPath.length];
const expectedType = typeof nextKey === "number" ? "array" : "object";
if (!isTypeOf(target[k], expectedType)) {
// value at path has another type
if (target[k] ? canChangeType : canCreate) {
target[k] = expectedType === "array" ? [] : {};
} else if (canIgnore) {
target = undefined;
newObj = obj; // return initial obj as-is
break;
} else {
throw new Error(`Value by path ${goodPath.join(".")} should have type ${expectedType} but got ${typeOf(target[k])}`);
}
}
goodPath.push(k);
target[k] = shallowCopy(target[k]);
target = target[k];
}
if (target) {
if (newValue === undefined) {
delete target[targetKey];
} else {
const oldValue = target[targetKey];
if (typeof newValue === "function") {
target[targetKey] = newValue(oldValue);
} else {
target[targetKey] = newValue;
}
}
}
return newObj;
};
export const mergeIn = (obj, mixin, opts) => {
const defaultOpts = {
canCreate: true, canChangeType: true,
deepCopyObj: false, deepCopyMixin: false,
arrayMergeMode: "merge", // "merge" | "join" | "joinMissing" | "joinRespectOrder" | "overwrite"
circularRefs: false,
specialSymbols: true,
};
opts = { ...defaultOpts, ...(opts ?? {}) };
const { deepCopyObj, deepCopyMixin, circularRefs, specialSymbols } = opts;
if (!isTypeOf(obj, "object")) {
throw new Error("obj is not an object");
}
if (!isTypeOf(mixin, "object")) {
throw new Error("mixin is not an object");
}
// symbols
const $v = Symbol.for("_v");
const $type = Symbol.for("_type");
const $canCreate = Symbol.for("_canCreate");
const $canChangeType = Symbol.for("_canChangeType");
const $arrayMergeMode = Symbol.for("_arrayMergeMode");
const newObj = deepCopyObj ? clone(obj, circularRefs) : shallowCopy(obj);
let newObjChanged = false;
const _process = (path, targetMix, target, {
isMixingArray, isMixingRealArray,
} = {}) => {
let indexDelta = 0;
for (const mk in targetMix) {
if (!isSafeKeyForObject(mk)) {
throw new Error(`mixin contains unsafe key ${mk}`);
// newObjChanged = false;
// break;
}
if (!hasSafeOwnProperty(targetMix, mk)) {
continue;
}
const k = isMixingArray ? Number(mk) + indexDelta : mk;
const useSymbols = specialSymbols && isObjectOrArray(targetMix[mk]);
let canCreate = opts.canCreate, canChangeType = opts.canChangeType, arrayMergeMode = opts.arrayMergeMode;
let targetMixValue = targetMix[mk];
let isMixValueExplicit = false;
let expectedType = typeOf(targetMixValue);
if (useSymbols) {
if ($v in targetMix[mk]) {
isMixValueExplicit = true;
targetMixValue = targetMix[mk][$v];
}
expectedType = targetMix[mk]?.[$type] || typeOf(targetMixValue);
canCreate = targetMix[mk]?.[$canCreate] ?? canCreate;
canChangeType = targetMix[mk]?.[$canChangeType] ?? canChangeType;
arrayMergeMode = targetMix[mk]?.[$arrayMergeMode] ?? arrayMergeMode;
if (expectedType === "array" && arrayMergeMode === "overwrite") {
isMixValueExplicit = true;
}
}
if (expectedType !== "array") {
arrayMergeMode = undefined;
}
if (!isTypeOf(target[k], expectedType)) {
// value at path has another type
if (target[k] ? canChangeType : canCreate) {
if (expectedType === "array" || expectedType === "object") {
target[k] = expectedType === "array" ? [] : {};
newObjChanged = true;
} else {
// primitive
}
} else {
continue;
}
}
if (expectedType === "array" || expectedType === "object") {
if (isMixValueExplicit) {
// deep copy from mix to target
newObjChanged = true;
target[k] = deepCopyMixin ? clone(targetMixValue, circularRefs) : shallowCopy(targetMixValue);
} else {
if (arrayMergeMode && ["join", "joinMissing", "joinRespectOrder"].includes(arrayMergeMode)) {
// join 2 arrays
newObjChanged = true;
const left = (deepCopyObj ? target[k] : clone(target[k], circularRefs));
let right = (deepCopyMixin ? clone(targetMixValue, circularRefs) : targetMixValue);
if (arrayMergeMode === "joinRespectOrder") {
target[k] = mergeArraysSmart(left, right);
} else {
if (arrayMergeMode === "joinMissing") {
right = right.filter(v => !left.includes(v));
}
target[k] = [ ...left, ...right ];
}
} else {
// recursive merge
if (!deepCopyObj) {
target[k] = shallowCopy(target[k]);
}
_process([...path, mk], targetMixValue, target[k], {
isMixingArray: expectedType === "array",
isMixingRealArray: expectedType === "array" && !targetMix[mk]?.[$type],
});
}
}
} else {
const needDelete = targetMixValue === undefined && !isMixingRealArray && !isMixValueExplicit;
const valueExists = (k in target);
if (needDelete) {
if (valueExists) {
newObjChanged = true;
if (Array.isArray(target)) {
target.splice(k, 1);
indexDelta--;
} else {
delete target[k];
}
}
} else {
newObjChanged = true;
target[k] = targetMixValue;
}
}
}
};
_process([], mixin, newObj);
return newObjChanged ? newObj : obj;
};
export const omit = (obj, keys) => {
return Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k)));
};
// RegExp.quote = function (str) {
// return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
// };
export const getOpCardinality = (opDef) => {
return opDef?.cardinality ?? 1;
};
// const immutableEqual = function(v1, v2) {
// if (v1 === v2) {
// return true;
// } else {
// return v1.equals(v2);
// }
// };
export const deepEqual = function(v1, v2) {
if (v1 === v2) {
return true;
} else if (Map.isMap(v1)) {
return v1.equals(v2);
} else {
return JSON.stringify(v1) == JSON.stringify(v2);
}
};
// //Do sets have same values?
// const eqSet = function (as, bs) {
// if (as.size !== bs.size) return false;
// for (var a of as) if (!bs.has(a)) return false;
// return true;
// };
// //Do arrays have same values?
// const eqArrSet = function (arr1, arr2) {
// return eqSet(new Set(arr1), new Set(arr2));
// };
export const shallowEqual = (a, b, deep = false) => {
if (a === b) {
return true;
} else if (Array.isArray(a))
return shallowEqualArrays(a, b, deep);
else if (Map.isMap(a))
return a.equals(b);
else if (typeof a == "object")
return shallowEqualObjects(a, b, deep);
else
return a === b;
};
function shallowEqualArrays(arrA, arrB, deep = false) {
if (arrA === arrB) {
return true;
}
if (!arrA || !arrB) {
return false;
}
var len = arrA.length;
if (arrB.length !== len) {
return false;
}
for (var i = 0; i < len; i++) {
var isEqual = deep ? shallowEqual(arrA[i], arrB[i], deep) : arrA[i] === arrB[i];
if (!isEqual) {
return false;
}
}
return true;
}
function shallowEqualObjects(objA, objB, deep = false) {
if (objA === objB) {
return true;
}
if (!objA || !objB) {
return false;
}
var aKeys = Object.keys(objA);
var bKeys = Object.keys(objB);
var len = aKeys.length;
if (bKeys.length !== len) {
return false;
}
for (var i = 0; i < len; i++) {
var key = aKeys[i];
var isEqual = deep ? shallowEqual(objA[key], objB[key], deep) : objA[key] === objB[key];
if (!isEqual) {
return false;
}
}
return true;
}
export const isImmutable = (v) => {
return typeof v === "object" && v !== null && typeof v.toJS === "function";
};
export const isImmutableList = (v) => {
return isImmutable(v) && Immutable.isList(v); // Immutable.isIndexed(v)
};
export function toImmutableList(v) {
return (isImmutableList(v) ? v : new Immutable.List(v));
}
export function applyToJS(v) {
return (isImmutable(v) ? v.toJS() : v);
}
export const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"); // $& means the whole matched string
};
export const mergeCustomizerNoArrays = (objValue, srcValue, _key, _object, _source, _stack) => {
if (Array.isArray(objValue)) {
return srcValue;
}
};
export const cleanJSX = (jsx) => {
const jsxKeys = ["$$typeof", "_owner", "_store", "ref", "key"];
const getName = (val) => {
if (typeof val === "string") {
return val;
} else if (typeof val === "function") {
return val.name;
}
return val;
};
if (jsx instanceof Array) {
return jsx.map((el, _i) => cleanJSX(el));
} else if (typeof jsx === "object" && jsx !== null) {
if (isDirtyJSX(jsx)) {
const cleaned = omit(jsx, jsxKeys);
if (cleaned.type) {
cleaned.type = getName(cleaned.type);
}
if (cleaned?.props?.children) {
cleaned.props.children = cleanJSX(cleaned.props.children);
}
return cleaned;
}
}
return jsx;
};
export const isDirtyJSX = (jsx) => {
return typeof jsx === "object"
&& jsx !== null
&& !Array.isArray(jsx)
&& Object.keys(jsx).includes("type")
&& Object.keys(jsx).includes("props") // even if {}
&& Object.keys(jsx).includes("key") // even if null
&& Object.keys(jsx).includes("ref") // even if null
&& Object.keys(jsx).includes("$$typeof"); // Symbol(react.element)
};
export const isJSX = (jsx) => (
typeof jsx === "object"
&& jsx !== null
&& !Array.isArray(jsx)
&& typeof jsx["type"] === "string"
&& Object.keys(jsx).includes("props")
);
export const isJsonLogic = (logic) => {
let isJL = typeof logic === "object" // An object
&& logic !== null // but not null
&& !Array.isArray(logic) // and not an array
&& Object.keys(logic).length === 1; // with exactly one key
if (isJL) {
// additional checks ?
}
return isJL;
};
export function sleep(delay) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
// [1, 4, 9] + [1, 5, 9] => [1, 4, 5, 9]
// Used for merging arrays of operators for different widgets of 1 type
export function mergeArraysSmart(arr1, arr2) {
if (!arr1) arr1 = [];
if (!arr2) arr2 = [];
return arr2
.map(op => [op, arr1.indexOf(op)])
.map(([op, ind], i, orig) => {
if (ind == -1) {
const next = orig.slice(i+1);
const prevs = orig.slice(0, i);
const after = [...prevs].reverse().find(([_cop, ci]) => ci != -1);
const prev = prevs[prevs.length - 1];
const before = next.find(([_cop, ci]) => ci != -1);
const isAfterDirectly = after && after === prevs[prevs.length-1];
const isBeforeDirectly = before && next === next[0];
if (isAfterDirectly) {
return [op, "after", after[0]];
} else if (isBeforeDirectly) {
return [op, "before", before[0]];
} else if (after) {
if (prev) {
return [op, "after", prev[0]];
}
return [op, "after", after[0]];
} else if (before) {
return [op, "before", before[0]];
} else {
return [op, "append", null];
}
} else {
// already exists
return null;
}
})
.filter(x => x !== null)
.reduce((acc, [newOp, rel, relOp]) => {
const ind = acc.indexOf(relOp);
if (acc.indexOf(newOp) === -1) {
if (ind > -1) {
// insert after or before
const offset = (rel === "after" ? 1 : 0);
acc.splice( ind + offset, 0, newOp );
} else {
// insert to end or start
acc.splice( (rel === "append" ? Infinity : 0), 0, newOp );
}
}
return acc;
}, arr1.slice());
}
export const deepFreeze = obj => {
if (typeof obj === "object" && obj !== null && !isDirtyJSX(obj)) {
Object.keys(obj).forEach(prop => {
if (prop !== "__cache") {
deepFreeze(obj[prop]);
}
});
Object.freeze(obj);
}
};
export const isJsonCompatible = (tpl, obj, bag = {}, path = []) => {
if (isObject(tpl)) {
if (tpl.var) {
bag[tpl.var] = obj;
return true;
}
if (!isObject(obj))
return false;
for (const k in tpl) {
const tv = tpl[k];
const ov = obj[k];
if (!isJsonCompatible(tv, ov, bag, [...path, k]))
return false;
}
return true;
} else if (Array.isArray(tpl)) {
if (!Array.isArray(obj))
return false;
for (let i = 0 ; i < tpl.length ; i++) {
const tv = tpl[i];
const ov = obj[i];
if (!isJsonCompatible(tv, ov, bag, [...path, i]))
return false;
}
return true;
} else {
return tpl === obj;
}
};
const isDev = () => (typeof process !== "undefined" && process?.env?.NODE_ENV == "development");
const isTest = () => (typeof process !== "undefined" && process?.env?.NODE_ENV_TEST == "true");
export const getLogger = (devMode) => {
if (isTest()) {
return {
...console,
log: () => {},
debug: () => {},
info: () => {},
};
}
const verbose = devMode != undefined ? devMode : isDev();
return verbose ? console : {
error: () => {},
log: () => {},
warn: () => {},
debug: () => {},
info: () => {},
};
};
export const getFirstDefined = (arr = []) => {
let ret;
for (let i = 0 ; i < arr.length ; i++) {
const v = arr[i];
if (v !== undefined) {
ret = v;
break;
}
}
return ret;
};
export const logger = getLogger();