-
-
Notifications
You must be signed in to change notification settings - Fork 13
/
parse-filter.js
126 lines (117 loc) · 4.42 KB
/
parse-filter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
function mergeParams(p1, p2) {
const params = new URLSearchParams(p1);
new URLSearchParams(p2).forEach((val, key) => {
params.append(key, val);
});
return params.toString();
}
// Helper for determining if a value is a primitive data structure
const isPrim = val =>
['string', 'number', 'boolean'].includes(typeof val) || val === null;
const logical = {
$and: 'AND',
$or: 'OR',
};
const comparison = {
$eq: '%3D',
$ne: '<>',
$gt: '>',
$gte: '>=',
$lt: '<',
$lte: '<=',
$in: 'IN',
$nin: 'NOT%20IN',
};
const truthyForms = [true, 1, 'true', 'TRUE', 'T'];
const falseyForms = [false, 0, 'false', 'FALSE', 'F'];
const booleanForms = [...truthyForms, ...falseyForms];
const booleanTransform = bool => (truthyForms.includes(bool) ? 1 : 0);
function transformRawValue(transform, raw) {
let value = raw;
if (typeof transform === 'function') {
value = transform(value);
}
if (booleanForms.includes(value)) {
value = booleanTransform(value);
}
return value;
}
export default function parseFilter(filter = {}, options = {}) {
const { filterTransforms = {} } = options;
function parseComparison(path, expr, comGroup = null, index = 0) {
const amp = index > 0 ? '&' : '';
const pre = `filter[${path}-${index}-filter][condition]`;
const membership = comGroup ? `&${pre}[memberOf]=${comGroup}` : '';
const [[op, rawValue], ...tail] = Object.entries(expr);
const val = transformRawValue(filterTransforms[path], rawValue);
if (val === null) {
const pathStr = `${amp}filter[${path}-filter][condition][path]=${path}`;
const opStr = `&filter[${path}-filter][condition][operator]=IS%20NULL`;
return pathStr + opStr + membership;
}
const urlEncodedOp = comparison[op];
if (!urlEncodedOp) throw new Error(`Invalid comparison operator: ${op}`);
const pathStr = `${amp}${pre}[path]=${path}`;
const opStr = `&${pre}[operator]=${urlEncodedOp}`;
const valStr = Array.isArray(val)
? val.reduce((substr, v, i) => `${substr}&${pre}[value][${i}]=${v}`, '')
: `&${pre}[value]=${val}`;
const str = pathStr + opStr + valStr + membership;
if (tail.length === 0) return str;
const nextExpr = Object.fromEntries(tail);
return str + parseComparison(path, nextExpr, comGroup, index + 1);
}
function parseLogic(op, filters, logicGroup, logicDepth) {
const label = `group-${logicDepth}`;
const conjunction = `&filter[${label}][group][conjunction]=${logical[op]}`;
const membership = logicGroup ? `&filter[${label}][condition][memberOf]=${logicGroup}` : '';
return filters.reduce(
// eslint-disable-next-line no-use-before-define
(params, f) => mergeParams(params, parser(f, label, logicDepth + 1)),
conjunction + membership,
);
}
function parseField(path, val, fieldGroup, fieldDepth) {
if (isPrim(val)) {
return parseComparison(path, { $eq: val }, fieldGroup);
}
if (Array.isArray(val) || '$or' in val) {
const arr = Array.isArray(val) ? val : val.$or;
if (!Array.isArray(arr)) {
throw new Error(`The value of \`${path}.$or\` must be an array. `
+ `Invalid constructor: ${arr.constructor.name}`);
}
const filters = arr.map(v => (isPrim(v) ? { [path]: v } : v));
return parseLogic('$or', filters, fieldGroup, fieldDepth + 1);
}
if ('$and' in val) {
if (!Array.isArray(val.$and)) {
throw new Error(`The value of \`${path}.$and\` must be an array. `
+ `Invalid constructor: ${val.$and.constructor.name}`);
}
return parseLogic('$and', val.$and, fieldGroup, fieldDepth + 1);
}
// Otherwise we assume val is an object and all its properties are comparison
// operators; parseComparison will throw if any property is NOT a comp op.
return parseComparison(path, val, fieldGroup);
}
const parser = (_filter, group, depth = 0) => {
if (Array.isArray(_filter)) {
return parseLogic('$or', _filter, group, depth);
}
let params = '';
const entries = Object.entries(_filter);
if (entries.length === 0) return params;
const [[key, val], ...rest] = entries;
if (['$and', '$or'].includes(key)) {
params = parseLogic(key, val, group, depth);
}
if (key && val !== undefined) {
params = parseField(key, val, group, depth);
}
if (rest.length === 0) return params;
const tailParams = parser(Object.fromEntries(rest));
return mergeParams(params, tailParams);
};
return parser(filter);
}