@lefcott/filter-json
Version:
Check, filter and compare JSONs
282 lines (269 loc) • 8.07 kB
JavaScript
const helper = require("./helper");
let prefix = "$";
let originalObj = null;
/**
* Configures the package
* @param {object} Config - Configuration.
* @param {string} [Config.prefix] - String prefix for checking things like $gt, $in, etc. Default to '$'
*/
const configure = (Config) => Config.prefix && (prefix = Config.prefix);
// Example: ([{k: 1}, {k: 1}, {k: 1}])
// -----> true
// Example: ([{k: 1}, {k: 1, j: 1}, {k: 1}])
// -----> false
const compareNObjects = (objects, excludeFields) => {
for (let k = 0; k < objects.length - 1; k += 1) {
if (!helper.compareObjects(objects[k], objects[k + 1], excludeFields)) {
return false;
}
}
return true;
};
// USE EXAMPLES:
// "hwd" ---> "hwd"
// "/hwd/" ---> /hwd/
const getRegexOrValue = (str) =>
typeof str === "string" && str[0] === str[str.length - 1] && str[0] === "/"
? new RegExp(str.substring(1, str.lastIndexOf("/")))
: str;
const compare = (a, b) => {
// Test RegExp if indicated in condition
if (b instanceof RegExp) return b.test(a);
if (b instanceof Object) {
b = { ...b };
let A;
const transform = `${prefix}transform`;
if (b[transform] instanceof Function) A = b[transform](a);
else if (typeof b[transform] === "string") A = eval(`(${b[transform]})`)(a);
delete b[transform];
A = A || a;
const keys = Object.keys(b);
const opKeys = keys.filter(
(key) => key.substring(0, prefix.length) === prefix
);
if (opKeys.length > 0) {
for (let k = 0; k < opKeys.length; k += 1) {
const key = opKeys[k];
if (!helper.compareValues(originalObj, A, key, b[key], prefix, check))
return false;
}
return true;
}
return helper.compareObjects(a, b, []);
}
return a === b;
};
const getValue = (search, obj) => {
const splittedKey = search.split(".");
let currObj = obj;
for (let m = 0; m < splittedKey.length; m += 1) {
currObj = currObj[splittedKey[m]];
if ([null, undefined].includes(currObj) && m < splittedKey.length - 1)
return null;
}
return currObj;
};
const setValue = (search, obj, value) => {
const splittedKey = typeof search === "string" ? search.split(".") : search;
if ([null, undefined].includes(obj)) obj = {};
if (splittedKey.length > 0)
obj[splittedKey[0]] = setValue(
splittedKey.slice(1),
obj[splittedKey[0]],
value
);
else obj = value;
return obj;
};
// USE EXAMPLES:
// {"k1": {"k2": "hello word"}}, {"k1.k2": "/word/"} ---> true
// {"k1": {"k2": "hello word"}}, {"k1.k2": "asdf"} ---> false
const check = (obj, condition) => {
if (!originalObj) originalObj = obj;
if (!(condition instanceof Object)) {
originalObj = null;
obj = getRegexOrValue(obj);
return obj === condition;
}
for (const conditionKey in condition) {
if (condition[conditionKey] === undefined) continue;
if (conditionKey === `${prefix}or`) {
let checkedOk = false;
for (let k = 0; k < condition[conditionKey].length; k += 1) {
if (check(obj, condition[conditionKey][k])) {
checkedOk = true;
break;
}
}
if (!checkedOk) {
originalObj = null;
return false;
}
continue;
}
if (conditionKey === `${prefix}and`) {
for (let k = 0; k < condition[conditionKey].length; k += 1) {
if (!check(obj, condition[conditionKey][k])) {
originalObj = null;
return false;
}
}
continue;
}
if (conditionKey === prefix) {
if (!compare(obj, condition[conditionKey])) {
originalObj = null;
return false;
}
continue;
}
const splittedKey = conditionKey.split(".");
let currObj = obj;
for (let m = 0; m < splittedKey.length; m += 1) {
currObj = currObj[splittedKey[m]];
if ([null, undefined].includes(currObj) && m < splittedKey.length - 1) {
originalObj = null;
return false;
}
}
condition[conditionKey] = getRegexOrValue(condition[conditionKey]);
if (!compare(currObj, condition[conditionKey])) {
originalObj = null;
return false;
}
}
originalObj = null;
return true;
};
const aggregate = (objs = [], search, type) => {
if (!objs.length) return [];
let last = getValue(search, objs[0] || {});
let [lastObj] = objs;
for (let k = 0; k < objs.length; k += 1) {
const curr = getValue(search, objs[k]);
if (type === "max") {
if (curr > last) {
last = curr;
lastObj = objs[k];
}
continue;
}
if (type === "min") {
if (curr < last) {
last = curr;
lastObj = objs[k];
}
continue;
}
console.error(`filter-json: Unknown aggregate type: "${type}"`);
return objs;
}
return [lastObj];
};
const filter = (objects, condition = {}, ...subFilters) => {
const objs = Array.isArray(objects) ? objects : [objects];
let agg;
if (condition._aggregate) {
agg = condition._aggregate;
delete condition._aggregate;
if (!agg.field) {
console.error(`filter-json: Invalid aggregate field: "${agg.field}"`);
return objects;
}
if (!agg.type) {
console.error(`filter-json: Invalid aggregate type: "${agg.type}"`);
return objects;
}
}
let after = objs.filter((obj) => check(obj, condition));
if (agg) after = aggregate(objs, agg.field, agg.type);
for (let k = 1; k < subFilters.length; k += 2) {
for (let m = 0; m < after.length; m += 1) {
const search = subFilters[k - 1];
const condition = subFilters[k];
let value;
if (!search) value = JSON.parse(JSON.stringify(after));
else if (search === prefix) value = after[m];
else value = getValue(search, after[m]);
if (!Array.isArray(value)) continue;
value = filter(value, condition);
if (!search) after = value;
else if (search === prefix) after[m] = value;
else setValue(search, after[m], value);
}
}
return after;
};
/**
* Parses a JSON from a string that may contain single quotes or no quotes on keys
* @param {string} str
*/
const parse = (str) => {
const ignoredRanges = helper.quoteIgnoreRanges(str, null);
const dotIndexes = helper.getIndexesIgnoring(str, ":", ignoredRanges);
const keyCharsRange = [
[48, 57],
[65, 90],
[97, 122],
];
const spaceChars = ["\n", "\t", " "];
var newQuotesIndexes = [];
var strOkQuotes = "",
ignoredIndexes = [];
for (var k = 0; k < ignoredRanges.length; k++) {
for (var m = 0; m < ignoredRanges[k].length; m++) {
ignoredIndexes[ignoredIndexes.length] = ignoredRanges[k][m];
}
}
for (var k = 0; k < str.length; k++) {
strOkQuotes += ignoredIndexes.indexOf(k) == -1 ? str[k] : '"';
}
str = strOkQuotes;
// Search and saves corresponding quotes indexes
for (var k = 0; k < dotIndexes.length; k++) {
var wasKeyChar = false;
for (var m = 1; true; m++) {
if (
helper.insideSomeRange(
str[dotIndexes[k] - m].charCodeAt(0),
keyCharsRange
)
) {
if (
str[dotIndexes[k] - m + 1] != "'" &&
str[dotIndexes[k] - m + 1] != '"' &&
!wasKeyChar
) {
newQuotesIndexes[newQuotesIndexes.length] = dotIndexes[k] - m;
}
wasKeyChar = true;
} else {
if (spaceChars.indexOf(str[dotIndexes[k] - m]) == -1) {
if (str[dotIndexes[k] - m] != "'" && str[dotIndexes[k] - m] != '"') {
newQuotesIndexes[newQuotesIndexes.length] = dotIndexes[k] - m;
}
break;
} else {
if (wasKeyChar) {
newQuotesIndexes[newQuotesIndexes.length] = dotIndexes[k] - m;
break;
}
}
wasKeyChar = false;
}
}
}
try {
return JSON.parse(helper.addCharsAfterIndexes(str, '"', newQuotesIndexes));
} catch (error) {
return error;
}
};
module.exports = {
configure,
checkJsonCondition: check,
filter,
compareN: compareNObjects,
parse,
getValue,
};