@open-mercato/shared 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/crud/advanced-filter-integration.js +48 -2
- package/dist/lib/crud/advanced-filter-integration.js.map +2 -2
- package/dist/lib/query/advanced-filter-tree.js +280 -0
- package/dist/lib/query/advanced-filter-tree.js.map +7 -0
- package/dist/lib/query/advanced-filter.js +168 -1
- package/dist/lib/query/advanced-filter.js.map +2 -2
- package/dist/lib/query/engine.js +4 -3
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +117 -69
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/crud/__tests__/advanced-filter-integration.test.ts +113 -0
- package/src/lib/crud/advanced-filter-integration.ts +85 -1
- package/src/lib/query/__tests__/advanced-filter-end-to-end.test.ts +367 -0
- package/src/lib/query/__tests__/advanced-filter-serialization.test.ts +218 -0
- package/src/lib/query/__tests__/advanced-filter-tree.test.ts +326 -0
- package/src/lib/query/__tests__/engine.scope-and-or.test.ts +9 -7
- package/src/lib/query/__tests__/join-utils.complexity.test.ts +43 -0
- package/src/lib/query/advanced-filter-tree.ts +328 -0
- package/src/lib/query/advanced-filter.ts +217 -0
- package/src/lib/query/engine.ts +4 -3
- package/src/lib/query/join-utils.ts +156 -70
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 204 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deserializeAdvancedFilter,
|
|
3
|
+
deserializeTree,
|
|
4
|
+
flatToTree,
|
|
5
|
+
convertAdvancedFilterToWhere
|
|
6
|
+
} from "../query/advanced-filter.js";
|
|
7
|
+
import { compileTreeToWhere } from "../query/advanced-filter-tree.js";
|
|
2
8
|
function splitOrClauses(filters) {
|
|
3
9
|
const directFilters = {};
|
|
4
10
|
let orClauses = null;
|
|
@@ -45,7 +51,47 @@ function mergeAdvancedFilters(existingFilters, query) {
|
|
|
45
51
|
}
|
|
46
52
|
return { ...existingFilters, ...advancedWhere };
|
|
47
53
|
}
|
|
54
|
+
function mergeAdvancedFilterTree(routeFilters, tree) {
|
|
55
|
+
if (!tree) return routeFilters;
|
|
56
|
+
const treeWhere = compileTreeToWhere(tree);
|
|
57
|
+
if (!treeWhere) return routeFilters;
|
|
58
|
+
if (Object.keys(routeFilters).length === 0) return treeWhere;
|
|
59
|
+
return { $and: [routeFilters, treeWhere] };
|
|
60
|
+
}
|
|
61
|
+
function mergeAdvancedFiltersFromQuery(routeFilters, query) {
|
|
62
|
+
const v2 = deserializeTree(query);
|
|
63
|
+
if (v2) return mergeAdvancedFilterTree(routeFilters, v2);
|
|
64
|
+
const flat = deserializeAdvancedFilter(query);
|
|
65
|
+
if (!flat) return routeFilters;
|
|
66
|
+
return mergeAdvancedFilterTree(routeFilters, flatToTree(flat));
|
|
67
|
+
}
|
|
68
|
+
function consumeAdvancedFilterState(query) {
|
|
69
|
+
let tree = deserializeTree(query);
|
|
70
|
+
if (!tree) {
|
|
71
|
+
const flat = deserializeAdvancedFilter(query);
|
|
72
|
+
if (flat) {
|
|
73
|
+
tree = flatToTree(flat);
|
|
74
|
+
if (process.env.NODE_ENV !== "production") {
|
|
75
|
+
const sentinel = "__OM_FILTER_V1_WARNED";
|
|
76
|
+
const g = globalThis;
|
|
77
|
+
if (!g[sentinel]) {
|
|
78
|
+
g[sentinel] = true;
|
|
79
|
+
console.warn("[advanced-filter] legacy v1 URL detected; auto-upgraded to v2 tree");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const key of Object.keys(query)) {
|
|
85
|
+
if (key.startsWith("filter[")) {
|
|
86
|
+
delete query[key];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return tree;
|
|
90
|
+
}
|
|
48
91
|
export {
|
|
49
|
-
|
|
92
|
+
consumeAdvancedFilterState,
|
|
93
|
+
mergeAdvancedFilterTree,
|
|
94
|
+
mergeAdvancedFilters,
|
|
95
|
+
mergeAdvancedFiltersFromQuery
|
|
50
96
|
};
|
|
51
97
|
//# sourceMappingURL=advanced-filter-integration.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/crud/advanced-filter-integration.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "AAAA,
|
|
4
|
+
"sourcesContent": ["import {\n deserializeAdvancedFilter,\n deserializeTree,\n flatToTree,\n convertAdvancedFilterToWhere,\n} from '../query/advanced-filter'\nimport { compileTreeToWhere, type AdvancedFilterTree } from '../query/advanced-filter-tree'\n\nfunction splitOrClauses(filters: Record<string, unknown>): {\n directFilters: Record<string, unknown>\n orClauses: Record<string, unknown>[] | null\n} {\n const directFilters: Record<string, unknown> = {}\n let orClauses: Record<string, unknown>[] | null = null\n\n for (const [key, value] of Object.entries(filters)) {\n if (key === '$or' && Array.isArray(value)) {\n orClauses = value.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))\n continue\n }\n directFilters[key] = value\n }\n\n return { directFilters, orClauses }\n}\n\n/**\n * Parse advanced filter query params and merge with existing Where filters.\n * Call this in buildFilters callback to support advanced filter query params.\n */\nexport function mergeAdvancedFilters(\n existingFilters: Record<string, unknown>,\n query: Record<string, unknown>,\n): Record<string, unknown> {\n const advancedState = deserializeAdvancedFilter(query)\n if (!advancedState) return existingFilters\n\n const advancedWhere = convertAdvancedFilterToWhere(advancedState)\n if (!Object.keys(advancedWhere).length) return existingFilters\n\n if ('$or' in advancedWhere) {\n if (!Object.keys(existingFilters).length) return advancedWhere\n\n const { directFilters: existingDirect, orClauses: existingOrClauses } = splitOrClauses(existingFilters)\n const advancedClauses = Array.isArray((advancedWhere as { $or?: unknown }).$or)\n ? ((advancedWhere as { $or: unknown[] }).$or).filter(\n (entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n : []\n\n if (!advancedClauses.length) return existingFilters\n\n if (!existingOrClauses?.length) {\n return {\n ...existingDirect,\n $or: advancedClauses,\n }\n }\n\n const combinedClauses: Record<string, unknown>[] = []\n for (const leftClause of existingOrClauses) {\n for (const rightClause of advancedClauses) {\n combinedClauses.push({\n ...leftClause,\n ...rightClause,\n })\n }\n }\n\n return combinedClauses.length\n ? {\n ...existingDirect,\n $or: combinedClauses,\n }\n : existingFilters\n }\n\n // AND logic: merge directly\n return { ...existingFilters, ...advancedWhere }\n}\n\n/**\n * Combine route-side filters with an advanced filter tree by AND.\n *\n * The compiled tree may itself contain nested `$and`/`$or` \u2014 the query engine's\n * normalize step (`compileToDnf` in join-utils.ts) expands those into Disjunctive\n * Normal Form, so the engine sees a single OR group with disjuncts that AND'd\n * leaves. Common-clause lifting in normalize keeps shared predicates ungrouped\n * (preserving the search-tokens optimization).\n */\nexport function mergeAdvancedFilterTree(\n routeFilters: Record<string, unknown>,\n tree: AdvancedFilterTree | null,\n): Record<string, unknown> {\n if (!tree) return routeFilters\n const treeWhere = compileTreeToWhere(tree)\n if (!treeWhere) return routeFilters\n if (Object.keys(routeFilters).length === 0) return treeWhere\n // AND-combine via $and so any nested $and/$or in either side reaches normalize unchanged.\n return { $and: [routeFilters, treeWhere] }\n}\n\n/**\n * Read-old-write-new helper: parse the request `query` for either a v2 tree URL\n * or a legacy v1 flat URL (auto-upgraded via flatToTree under SQL precedence),\n * and AND-combine the result with the route's existing filters.\n */\nexport function mergeAdvancedFiltersFromQuery(\n routeFilters: Record<string, unknown>,\n query: Record<string, unknown>,\n): Record<string, unknown> {\n const v2 = deserializeTree(query)\n if (v2) return mergeAdvancedFilterTree(routeFilters, v2)\n const flat = deserializeAdvancedFilter(query)\n if (!flat) return routeFilters\n return mergeAdvancedFilterTree(routeFilters, flatToTree(flat))\n}\n\n/**\n * Returns the request's advanced filter as a tree, after stripping the\n * `filter[...]` keys from the query map. Mirrors `mergeAdvancedFiltersFromQuery`\n * but exposes the tree so callers can apply additional shaping (e.g. mapping to\n * `query.ids`, narrowing by feature flags, or surfacing the tree to enrichers)\n * before merging into the route filter.\n *\n * - Detects v2 tree URL params first.\n * - Falls back to legacy v1 flat params, upgraded via `flatToTree` under\n * standard SQL precedence (AND binds tighter than OR).\n * - Mutates `query` in place: every `filter[...]` key is removed after parsing.\n *\n * Returns `null` when no advanced-filter query params are present.\n */\nexport function consumeAdvancedFilterState(query: Record<string, unknown>): AdvancedFilterTree | null {\n let tree: AdvancedFilterTree | null = deserializeTree(query)\n if (!tree) {\n const flat = deserializeAdvancedFilter(query)\n if (flat) {\n tree = flatToTree(flat)\n if (process.env.NODE_ENV !== 'production') {\n const sentinel = '__OM_FILTER_V1_WARNED'\n const g = globalThis as Record<string, unknown>\n if (!g[sentinel]) {\n g[sentinel] = true\n // eslint-disable-next-line no-console\n console.warn('[advanced-filter] legacy v1 URL detected; auto-upgraded to v2 tree')\n }\n }\n }\n }\n\n for (const key of Object.keys(query)) {\n if (key.startsWith('filter[')) {\n delete query[key]\n }\n }\n\n return tree\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAAmD;AAE5D,SAAS,eAAe,SAGtB;AACA,QAAM,gBAAyC,CAAC;AAChD,MAAI,YAA8C;AAElD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,QAAI,QAAQ,SAAS,MAAM,QAAQ,KAAK,GAAG;AACzC,kBAAY,MAAM,OAAO,CAAC,UAA4C,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,CAAC;AAC1I;AAAA,IACF;AACA,kBAAc,GAAG,IAAI;AAAA,EACvB;AAEA,SAAO,EAAE,eAAe,UAAU;AACpC;AAMO,SAAS,qBACd,iBACA,OACyB;AACzB,QAAM,gBAAgB,0BAA0B,KAAK;AACrD,MAAI,CAAC,cAAe,QAAO;AAE3B,QAAM,gBAAgB,6BAA6B,aAAa;AAChE,MAAI,CAAC,OAAO,KAAK,aAAa,EAAE,OAAQ,QAAO;AAE/C,MAAI,SAAS,eAAe;AAC1B,QAAI,CAAC,OAAO,KAAK,eAAe,EAAE,OAAQ,QAAO;AAEjD,UAAM,EAAE,eAAe,gBAAgB,WAAW,kBAAkB,IAAI,eAAe,eAAe;AACtG,UAAM,kBAAkB,MAAM,QAAS,cAAoC,GAAG,IACxE,cAAqC,IAAK;AAAA,MAC1C,CAAC,UAA4C,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAAA,IAClH,IACA,CAAC;AAEL,QAAI,CAAC,gBAAgB,OAAQ,QAAO;AAEpC,QAAI,CAAC,mBAAmB,QAAQ;AAC9B,aAAO;AAAA,QACL,GAAG;AAAA,QACH,KAAK;AAAA,MACP;AAAA,IACF;AAEA,UAAM,kBAA6C,CAAC;AACpD,eAAW,cAAc,mBAAmB;AAC1C,iBAAW,eAAe,iBAAiB;AACzC,wBAAgB,KAAK;AAAA,UACnB,GAAG;AAAA,UACH,GAAG;AAAA,QACL,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO,gBAAgB,SACnB;AAAA,MACE,GAAG;AAAA,MACH,KAAK;AAAA,IACP,IACA;AAAA,EACN;AAGA,SAAO,EAAE,GAAG,iBAAiB,GAAG,cAAc;AAChD;AAWO,SAAS,wBACd,cACA,MACyB;AACzB,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,YAAY,mBAAmB,IAAI;AACzC,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI,OAAO,KAAK,YAAY,EAAE,WAAW,EAAG,QAAO;AAEnD,SAAO,EAAE,MAAM,CAAC,cAAc,SAAS,EAAE;AAC3C;AAOO,SAAS,8BACd,cACA,OACyB;AACzB,QAAM,KAAK,gBAAgB,KAAK;AAChC,MAAI,GAAI,QAAO,wBAAwB,cAAc,EAAE;AACvD,QAAM,OAAO,0BAA0B,KAAK;AAC5C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,wBAAwB,cAAc,WAAW,IAAI,CAAC;AAC/D;AAgBO,SAAS,2BAA2B,OAA2D;AACpG,MAAI,OAAkC,gBAAgB,KAAK;AAC3D,MAAI,CAAC,MAAM;AACT,UAAM,OAAO,0BAA0B,KAAK;AAC5C,QAAI,MAAM;AACR,aAAO,WAAW,IAAI;AACtB,UAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,cAAM,WAAW;AACjB,cAAM,IAAI;AACV,YAAI,CAAC,EAAE,QAAQ,GAAG;AAChB,YAAE,QAAQ,IAAI;AAEd,kBAAQ,KAAK,oEAAoE;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,QAAI,IAAI,WAAW,SAAS,GAAG;AAC7B,aAAO,MAAM,GAAG;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { isValuelessOperator } from "./advanced-filter.js";
|
|
2
|
+
import { escapeLikePattern } from "../db/escapeLikePattern.js";
|
|
3
|
+
const TREE_LIMITS = {
|
|
4
|
+
maxGroupLevel: 3,
|
|
5
|
+
// root = level 1; deepest nested group = level 3
|
|
6
|
+
maxChildrenPerGroup: 15,
|
|
7
|
+
// counts both rules and nested groups
|
|
8
|
+
maxTotalRules: 50
|
|
9
|
+
};
|
|
10
|
+
function createEmptyTree() {
|
|
11
|
+
return {
|
|
12
|
+
root: {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
type: "group",
|
|
15
|
+
combinator: "and",
|
|
16
|
+
children: []
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function validateTreeLimits(tree) {
|
|
21
|
+
let totalRules = 0;
|
|
22
|
+
function walk(node, level) {
|
|
23
|
+
if (node.type === "rule") {
|
|
24
|
+
totalRules += 1;
|
|
25
|
+
return { ok: true };
|
|
26
|
+
}
|
|
27
|
+
if (level > TREE_LIMITS.maxGroupLevel) return { ok: false, reason: "depth" };
|
|
28
|
+
if (node.children.length > TREE_LIMITS.maxChildrenPerGroup) return { ok: false, reason: "width" };
|
|
29
|
+
for (const child of node.children) {
|
|
30
|
+
const r2 = walk(child, level + 1);
|
|
31
|
+
if (!r2.ok) return r2;
|
|
32
|
+
}
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
const r = walk(tree.root, 1);
|
|
36
|
+
if (!r.ok) return r;
|
|
37
|
+
if (totalRules > TREE_LIMITS.maxTotalRules) return { ok: false, reason: "total" };
|
|
38
|
+
return { ok: true };
|
|
39
|
+
}
|
|
40
|
+
function normalizeSingleValue(value) {
|
|
41
|
+
if (typeof value !== "string") return value;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
44
|
+
}
|
|
45
|
+
function normalizeListValue(value) {
|
|
46
|
+
const list = Array.isArray(value) ? value : [value];
|
|
47
|
+
return list.map(normalizeSingleValue).filter((v) => v !== null);
|
|
48
|
+
}
|
|
49
|
+
function compileRule(rule) {
|
|
50
|
+
if (!rule.field || !rule.operator) return null;
|
|
51
|
+
const filter = {};
|
|
52
|
+
switch (rule.operator) {
|
|
53
|
+
case "is":
|
|
54
|
+
case "equals": {
|
|
55
|
+
const v = normalizeSingleValue(rule.value);
|
|
56
|
+
if (v === null) return null;
|
|
57
|
+
filter[rule.field] = { $eq: v };
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "is_not":
|
|
61
|
+
case "not_equals": {
|
|
62
|
+
const v = normalizeSingleValue(rule.value);
|
|
63
|
+
if (v === null) return null;
|
|
64
|
+
filter[rule.field] = { $ne: v };
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "contains": {
|
|
68
|
+
const v = normalizeSingleValue(rule.value);
|
|
69
|
+
if (v === null) return null;
|
|
70
|
+
filter[rule.field] = { $ilike: `%${escapeLikePattern(String(v))}%` };
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "does_not_contain": {
|
|
74
|
+
const v = normalizeSingleValue(rule.value);
|
|
75
|
+
if (v === null) return null;
|
|
76
|
+
filter[rule.field] = { $not: { $ilike: `%${escapeLikePattern(String(v))}%` } };
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "starts_with": {
|
|
80
|
+
const v = normalizeSingleValue(rule.value);
|
|
81
|
+
if (v === null) return null;
|
|
82
|
+
filter[rule.field] = { $ilike: `${escapeLikePattern(String(v))}%` };
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "ends_with": {
|
|
86
|
+
const v = normalizeSingleValue(rule.value);
|
|
87
|
+
if (v === null) return null;
|
|
88
|
+
filter[rule.field] = { $ilike: `%${escapeLikePattern(String(v))}` };
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "is_empty":
|
|
92
|
+
filter[rule.field] = { $exists: false };
|
|
93
|
+
break;
|
|
94
|
+
case "is_not_empty":
|
|
95
|
+
filter[rule.field] = { $exists: true };
|
|
96
|
+
break;
|
|
97
|
+
case "greater_than": {
|
|
98
|
+
const v = normalizeSingleValue(rule.value);
|
|
99
|
+
if (v === null) return null;
|
|
100
|
+
filter[rule.field] = { $gt: v };
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "less_than": {
|
|
104
|
+
const v = normalizeSingleValue(rule.value);
|
|
105
|
+
if (v === null) return null;
|
|
106
|
+
filter[rule.field] = { $lt: v };
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "greater_or_equal": {
|
|
110
|
+
const v = normalizeSingleValue(rule.value);
|
|
111
|
+
if (v === null) return null;
|
|
112
|
+
filter[rule.field] = { $gte: v };
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "less_or_equal": {
|
|
116
|
+
const v = normalizeSingleValue(rule.value);
|
|
117
|
+
if (v === null) return null;
|
|
118
|
+
filter[rule.field] = { $lte: v };
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "between": {
|
|
122
|
+
if (Array.isArray(rule.value) && rule.value.length === 2) {
|
|
123
|
+
const start = normalizeSingleValue(rule.value[0]);
|
|
124
|
+
const end = normalizeSingleValue(rule.value[1]);
|
|
125
|
+
if (start === null && end === null) return null;
|
|
126
|
+
if (start !== null && end !== null) filter[rule.field] = { $gte: start, $lte: end };
|
|
127
|
+
else if (start !== null) filter[rule.field] = { $gte: start };
|
|
128
|
+
else filter[rule.field] = { $lte: end };
|
|
129
|
+
} else {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "is_before": {
|
|
135
|
+
const v = normalizeSingleValue(rule.value);
|
|
136
|
+
if (v === null) return null;
|
|
137
|
+
filter[rule.field] = { $lt: v };
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "is_after": {
|
|
141
|
+
const v = normalizeSingleValue(rule.value);
|
|
142
|
+
if (v === null) return null;
|
|
143
|
+
filter[rule.field] = { $gt: v };
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "is_any_of":
|
|
147
|
+
case "has_any_of": {
|
|
148
|
+
const list = normalizeListValue(rule.value);
|
|
149
|
+
if (list.length === 0) return null;
|
|
150
|
+
filter[rule.field] = { $in: list };
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "is_none_of":
|
|
154
|
+
case "has_none_of": {
|
|
155
|
+
const list = normalizeListValue(rule.value);
|
|
156
|
+
if (list.length === 0) return null;
|
|
157
|
+
filter[rule.field] = { $nin: list };
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "has_all_of": {
|
|
161
|
+
const list = normalizeListValue(rule.value);
|
|
162
|
+
if (list.length === 0) return null;
|
|
163
|
+
filter[rule.field] = { $contains: list };
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "is_true":
|
|
167
|
+
filter[rule.field] = { $eq: true };
|
|
168
|
+
break;
|
|
169
|
+
case "is_false":
|
|
170
|
+
filter[rule.field] = { $eq: false };
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
return Object.keys(filter).length > 0 ? filter : null;
|
|
174
|
+
}
|
|
175
|
+
function compileTreeToWhere(tree) {
|
|
176
|
+
return compileGroup(tree.root);
|
|
177
|
+
}
|
|
178
|
+
function compileGroup(node) {
|
|
179
|
+
const compiled = node.children.map((child) => child.type === "rule" ? compileRule(child) : compileGroup(child)).filter((c) => c !== null);
|
|
180
|
+
if (compiled.length === 0) return null;
|
|
181
|
+
if (compiled.length === 1) return compiled[0];
|
|
182
|
+
return node.combinator === "or" ? { $or: compiled } : { $and: compiled };
|
|
183
|
+
}
|
|
184
|
+
function makeRuleTree(rule) {
|
|
185
|
+
const ruleNode = {
|
|
186
|
+
id: crypto.randomUUID(),
|
|
187
|
+
type: "rule",
|
|
188
|
+
field: rule.field,
|
|
189
|
+
operator: rule.operator,
|
|
190
|
+
value: rule.value
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
root: {
|
|
194
|
+
id: crypto.randomUUID(),
|
|
195
|
+
type: "group",
|
|
196
|
+
combinator: "and",
|
|
197
|
+
children: [ruleNode]
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function stripRuntimeMetadataDeep(node) {
|
|
202
|
+
if (node.type === "rule") {
|
|
203
|
+
const { addedAt: _ignored2, ...rest2 } = node;
|
|
204
|
+
return rest2;
|
|
205
|
+
}
|
|
206
|
+
const { addedAt: _ignored, children, ...rest } = node;
|
|
207
|
+
const cleanChildren = children.map((c) => stripRuntimeMetadataDeep(c));
|
|
208
|
+
return { ...rest, children: cleanChildren };
|
|
209
|
+
}
|
|
210
|
+
function serializeTreeForPersist(tree) {
|
|
211
|
+
return { v: 2, root: stripRuntimeMetadataDeep(tree.root) };
|
|
212
|
+
}
|
|
213
|
+
function isPersistedFilterTree(value) {
|
|
214
|
+
if (!value || typeof value !== "object") return false;
|
|
215
|
+
const record = value;
|
|
216
|
+
if (record.v !== 2) return false;
|
|
217
|
+
const root = record.root;
|
|
218
|
+
if (!root || typeof root !== "object") return false;
|
|
219
|
+
const r = root;
|
|
220
|
+
return r.type === "group" && (r.combinator === "and" || r.combinator === "or") && Array.isArray(r.children);
|
|
221
|
+
}
|
|
222
|
+
function deserializeTreeFromPersist(value) {
|
|
223
|
+
if (!isPersistedFilterTree(value)) return null;
|
|
224
|
+
return { root: value.root };
|
|
225
|
+
}
|
|
226
|
+
function treeToFlat(tree) {
|
|
227
|
+
const conditions = [];
|
|
228
|
+
function walk(group, parentJoin) {
|
|
229
|
+
for (const child of group.children) {
|
|
230
|
+
if (child.type === "rule") {
|
|
231
|
+
conditions.push({
|
|
232
|
+
id: child.id,
|
|
233
|
+
field: child.field,
|
|
234
|
+
operator: child.operator,
|
|
235
|
+
value: child.value,
|
|
236
|
+
// First rule overall gets 'and' (it has no left neighbor); the rest
|
|
237
|
+
// inherit the parent group's combinator.
|
|
238
|
+
join: conditions.length === 0 ? "and" : parentJoin
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
walk(child, child.combinator);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
walk(tree.root, tree.root.combinator);
|
|
246
|
+
return { logic: tree.root.combinator, conditions };
|
|
247
|
+
}
|
|
248
|
+
function makeMultiRuleTree(rules, combinator = "and") {
|
|
249
|
+
return {
|
|
250
|
+
root: {
|
|
251
|
+
id: crypto.randomUUID(),
|
|
252
|
+
type: "group",
|
|
253
|
+
combinator,
|
|
254
|
+
children: rules.map((rule) => {
|
|
255
|
+
const node = {
|
|
256
|
+
id: crypto.randomUUID(),
|
|
257
|
+
type: "rule",
|
|
258
|
+
field: rule.field,
|
|
259
|
+
operator: rule.operator,
|
|
260
|
+
value: rule.value
|
|
261
|
+
};
|
|
262
|
+
return node;
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export {
|
|
268
|
+
TREE_LIMITS,
|
|
269
|
+
compileTreeToWhere,
|
|
270
|
+
createEmptyTree,
|
|
271
|
+
deserializeTreeFromPersist,
|
|
272
|
+
isPersistedFilterTree,
|
|
273
|
+
isValuelessOperator,
|
|
274
|
+
makeMultiRuleTree,
|
|
275
|
+
makeRuleTree,
|
|
276
|
+
serializeTreeForPersist,
|
|
277
|
+
treeToFlat,
|
|
278
|
+
validateTreeLimits
|
|
279
|
+
};
|
|
280
|
+
//# sourceMappingURL=advanced-filter-tree.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/query/advanced-filter-tree.ts"],
|
|
4
|
+
"sourcesContent": ["// packages/shared/src/lib/query/advanced-filter-tree.ts\nimport type { FilterOperator } from './advanced-filter'\nimport { isValuelessOperator } from './advanced-filter'\nimport { escapeLikePattern } from '../db/escapeLikePattern'\n\nexport type FilterCombinator = 'and' | 'or'\n\nexport type FilterRule = {\n id: string\n type: 'rule'\n field: string\n operator: FilterOperator\n value: unknown\n // Runtime-only metadata used by the editor reducer (e.g. removeLast) to find\n // the most recently inserted node. MUST NOT be serialized into URL or persisted state.\n addedAt?: number\n}\n\nexport type FilterGroup = {\n id: string\n type: 'group'\n combinator: FilterCombinator\n children: Array<FilterRule | FilterGroup>\n // Runtime-only metadata used by the editor reducer (e.g. removeLast) to find\n // the most recently inserted node. MUST NOT be serialized into URL or persisted state.\n addedAt?: number\n}\n\nexport type AdvancedFilterTree = {\n root: FilterGroup\n}\n\nexport const TREE_LIMITS = {\n maxGroupLevel: 3, // root = level 1; deepest nested group = level 3\n maxChildrenPerGroup: 15, // counts both rules and nested groups\n maxTotalRules: 50,\n} as const\n\nexport function createEmptyTree(): AdvancedFilterTree {\n return {\n root: {\n id: crypto.randomUUID(),\n type: 'group',\n combinator: 'and',\n children: [],\n },\n }\n}\n\nexport type ValidationResult =\n | { ok: true }\n | { ok: false; reason: 'depth' | 'width' | 'total' }\n\nexport function validateTreeLimits(tree: AdvancedFilterTree): ValidationResult {\n let totalRules = 0\n function walk(node: FilterRule | FilterGroup, level: number): ValidationResult {\n if (node.type === 'rule') {\n totalRules += 1\n return { ok: true }\n }\n if (level > TREE_LIMITS.maxGroupLevel) return { ok: false, reason: 'depth' }\n if (node.children.length > TREE_LIMITS.maxChildrenPerGroup) return { ok: false, reason: 'width' }\n for (const child of node.children) {\n const r = walk(child, level + 1)\n if (!r.ok) return r\n }\n return { ok: true }\n }\n const r = walk(tree.root, 1)\n if (!r.ok) return r\n if (totalRules > TREE_LIMITS.maxTotalRules) return { ok: false, reason: 'total' }\n return { ok: true }\n}\n\nfunction normalizeSingleValue(value: unknown): unknown {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeListValue(value: unknown): unknown[] {\n const list = Array.isArray(value) ? value : [value]\n return list.map(normalizeSingleValue).filter((v) => v !== null)\n}\n\nfunction compileRule(rule: FilterRule): Record<string, unknown> | null {\n if (!rule.field || !rule.operator) return null\n const filter: Record<string, unknown> = {}\n switch (rule.operator) {\n case 'is':\n case 'equals': {\n const v = normalizeSingleValue(rule.value)\n if (v === null) return null\n filter[rule.field] = { $eq: v }; break\n }\n case 'is_not':\n case 'not_equals': {\n const v = normalizeSingleValue(rule.value)\n if (v === null) return null\n filter[rule.field] = { $ne: v }; break\n }\n case 'contains': {\n const v = normalizeSingleValue(rule.value)\n if (v === null) return null\n filter[rule.field] = { $ilike: `%${escapeLikePattern(String(v))}%` }; break\n }\n case 'does_not_contain': {\n const v = normalizeSingleValue(rule.value)\n if (v === null) return null\n filter[rule.field] = { $not: { $ilike: `%${escapeLikePattern(String(v))}%` } }; break\n }\n case 'starts_with': {\n const v = normalizeSingleValue(rule.value)\n if (v === null) return null\n filter[rule.field] = { $ilike: `${escapeLikePattern(String(v))}%` }; break\n }\n case 'ends_with': {\n const v = normalizeSingleValue(rule.value)\n if (v === null) return null\n filter[rule.field] = { $ilike: `%${escapeLikePattern(String(v))}` }; break\n }\n case 'is_empty': filter[rule.field] = { $exists: false }; break\n case 'is_not_empty': filter[rule.field] = { $exists: true }; break\n case 'greater_than': { const v = normalizeSingleValue(rule.value); if (v === null) return null; filter[rule.field] = { $gt: v }; break }\n case 'less_than': { const v = normalizeSingleValue(rule.value); if (v === null) return null; filter[rule.field] = { $lt: v }; break }\n case 'greater_or_equal': { const v = normalizeSingleValue(rule.value); if (v === null) return null; filter[rule.field] = { $gte: v }; break }\n case 'less_or_equal': { const v = normalizeSingleValue(rule.value); if (v === null) return null; filter[rule.field] = { $lte: v }; break }\n case 'between': {\n if (Array.isArray(rule.value) && rule.value.length === 2) {\n const start = normalizeSingleValue(rule.value[0])\n const end = normalizeSingleValue(rule.value[1])\n if (start === null && end === null) return null\n if (start !== null && end !== null) filter[rule.field] = { $gte: start, $lte: end }\n else if (start !== null) filter[rule.field] = { $gte: start }\n else filter[rule.field] = { $lte: end }\n } else {\n return null\n }\n break\n }\n case 'is_before': { const v = normalizeSingleValue(rule.value); if (v === null) return null; filter[rule.field] = { $lt: v }; break }\n case 'is_after': { const v = normalizeSingleValue(rule.value); if (v === null) return null; filter[rule.field] = { $gt: v }; break }\n case 'is_any_of':\n case 'has_any_of': {\n const list = normalizeListValue(rule.value)\n if (list.length === 0) return null\n filter[rule.field] = { $in: list }; break\n }\n case 'is_none_of':\n case 'has_none_of': {\n const list = normalizeListValue(rule.value)\n if (list.length === 0) return null\n filter[rule.field] = { $nin: list }; break\n }\n case 'has_all_of': {\n const list = normalizeListValue(rule.value)\n if (list.length === 0) return null\n filter[rule.field] = { $contains: list }; break\n }\n case 'is_true': filter[rule.field] = { $eq: true }; break\n case 'is_false': filter[rule.field] = { $eq: false }; break\n }\n return Object.keys(filter).length > 0 ? filter : null\n}\n\nexport function compileTreeToWhere(tree: AdvancedFilterTree): Record<string, unknown> | null {\n return compileGroup(tree.root)\n}\n\nfunction compileGroup(node: FilterGroup): Record<string, unknown> | null {\n const compiled = node.children\n .map((child) => (child.type === 'rule' ? compileRule(child) : compileGroup(child)))\n .filter((c): c is Record<string, unknown> => c !== null)\n if (compiled.length === 0) return null\n if (compiled.length === 1) return compiled[0]\n return node.combinator === 'or' ? { $or: compiled } : { $and: compiled }\n}\n\n// Re-export isValuelessOperator so consumers of this module can access it\nexport { isValuelessOperator }\n\n/**\n * Build a single-rule tree wrapped in an AND root group.\n * Useful for quick-filter presets that apply one rule.\n */\nexport function makeRuleTree(rule: { field: string; operator: FilterOperator; value: unknown }): AdvancedFilterTree {\n const ruleNode: FilterRule = {\n id: crypto.randomUUID(),\n type: 'rule',\n field: rule.field,\n operator: rule.operator,\n value: rule.value,\n }\n return {\n root: {\n id: crypto.randomUUID(),\n type: 'group',\n combinator: 'and',\n children: [ruleNode],\n },\n }\n}\n\n/**\n * Persisted shape of an `AdvancedFilterTree` inside `PerspectiveSettings.filters`.\n * The `v: 2` marker disambiguates the tree from legacy flat `FilterValues` and\n * from the plain `{root: ...}` shape. Runtime-only `addedAt` is stripped before\n * persisting so saved perspectives are deterministic across reloads.\n */\nexport type PersistedFilterTree = {\n v: 2\n root: FilterGroup\n}\n\nfunction stripRuntimeMetadataDeep<T extends FilterRule | FilterGroup>(node: T): T {\n if (node.type === 'rule') {\n const { addedAt: _ignored, ...rest } = node as FilterRule & { addedAt?: number }\n return rest as T\n }\n const { addedAt: _ignored, children, ...rest } = node as FilterGroup & { addedAt?: number }\n const cleanChildren = children.map((c) => stripRuntimeMetadataDeep(c))\n return { ...rest, children: cleanChildren } as T\n}\n\n/** Serialize a tree for persistence (perspective settings, exports, etc.). */\nexport function serializeTreeForPersist(tree: AdvancedFilterTree): PersistedFilterTree {\n return { v: 2, root: stripRuntimeMetadataDeep(tree.root) }\n}\n\n/** Type guard: does `value` look like a persisted filter tree? */\nexport function isPersistedFilterTree(value: unknown): value is PersistedFilterTree {\n if (!value || typeof value !== 'object') return false\n const record = value as Record<string, unknown>\n if (record.v !== 2) return false\n const root = record.root\n if (!root || typeof root !== 'object') return false\n const r = root as Record<string, unknown>\n return r.type === 'group' && (r.combinator === 'and' || r.combinator === 'or') && Array.isArray(r.children)\n}\n\n/**\n * Restore a tree from its persisted shape. Returns `null` when the input is\n * not a recognizable persisted tree \u2014 callers should fall back to legacy\n * filter handling in that case.\n */\nexport function deserializeTreeFromPersist(value: unknown): AdvancedFilterTree | null {\n if (!isPersistedFilterTree(value)) return null\n return { root: value.root }\n}\n\n/**\n * Best-effort back-conversion of a tree to the legacy flat `AdvancedFilterState`\n * shape (`{logic, conditions[]}`). The flat shape only supports a single level\n * of AND/OR joining between sibling conditions; nested groups are flattened by\n * walking all leaf rules into the top level. Use this only for the legacy\n * `DataTable.advancedFilter` BC bridge \u2014 new code should keep the tree shape.\n *\n * The function picks `logic` from the root group's combinator and uses each\n * rule's parent-group combinator as its `join`. The first rule's `join` is\n * always `'and'` (it has no left neighbor).\n */\nexport function treeToFlat(tree: AdvancedFilterTree): {\n logic: 'and' | 'or'\n conditions: Array<{\n id: string\n field: string\n operator: import('./advanced-filter').FilterOperator\n value: unknown\n join: 'and' | 'or'\n }>\n} {\n const conditions: Array<{\n id: string\n field: string\n operator: import('./advanced-filter').FilterOperator\n value: unknown\n join: 'and' | 'or'\n }> = []\n\n function walk(group: FilterGroup, parentJoin: 'and' | 'or') {\n for (const child of group.children) {\n if (child.type === 'rule') {\n conditions.push({\n id: child.id,\n field: child.field,\n operator: child.operator,\n value: child.value,\n // First rule overall gets 'and' (it has no left neighbor); the rest\n // inherit the parent group's combinator.\n join: conditions.length === 0 ? 'and' : parentJoin,\n })\n } else {\n walk(child, child.combinator)\n }\n }\n }\n walk(tree.root, tree.root.combinator)\n\n return { logic: tree.root.combinator, conditions }\n}\n\n/**\n * Build a multi-rule tree wrapped in a root group with the given combinator.\n * Useful for quick-filter presets that combine multiple rules (e.g.\n * `status is win AND close_date is_after start_of_quarter`).\n */\nexport function makeMultiRuleTree(\n rules: Array<{ field: string; operator: FilterOperator; value: unknown }>,\n combinator: FilterCombinator = 'and',\n): AdvancedFilterTree {\n return {\n root: {\n id: crypto.randomUUID(),\n type: 'group',\n combinator,\n children: rules.map((rule) => {\n const node: FilterRule = {\n id: crypto.randomUUID(),\n type: 'rule',\n field: rule.field,\n operator: rule.operator,\n value: rule.value,\n }\n return node\n }),\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["r", "_ignored", "rest"]
|
|
7
|
+
}
|
|
@@ -259,15 +259,182 @@ function convertAdvancedFilterToWhere(state) {
|
|
|
259
259
|
}
|
|
260
260
|
return clauses.length > 1 ? { $or: clauses } : clauses[0];
|
|
261
261
|
}
|
|
262
|
+
function serializeTree(tree) {
|
|
263
|
+
if (!treeHasRules(tree.root)) return {};
|
|
264
|
+
const out = { "filter[v]": "2" };
|
|
265
|
+
serializeTreeGroup(tree.root, "filter[root]", out);
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
function treeHasRules(group) {
|
|
269
|
+
return group.children.some((child) => child.type === "rule" || treeHasRules(child));
|
|
270
|
+
}
|
|
271
|
+
function serializeTreeGroup(group, prefix, out) {
|
|
272
|
+
out[`${prefix}[combinator]`] = group.combinator;
|
|
273
|
+
group.children.forEach((child, idx) => {
|
|
274
|
+
const childPrefix = `${prefix}[children][${idx}]`;
|
|
275
|
+
if (child.type === "rule") serializeTreeRule(child, childPrefix, out);
|
|
276
|
+
else {
|
|
277
|
+
out[`${childPrefix}[type]`] = "group";
|
|
278
|
+
serializeTreeGroup(child, childPrefix, out);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function serializeTreeRule(rule, prefix, out) {
|
|
283
|
+
out[`${prefix}[type]`] = "rule";
|
|
284
|
+
out[`${prefix}[field]`] = rule.field;
|
|
285
|
+
out[`${prefix}[op]`] = rule.operator;
|
|
286
|
+
if (!isValuelessOperator(rule.operator) && rule.value != null) {
|
|
287
|
+
out[`${prefix}[value]`] = typeof rule.value === "object" ? JSON.stringify(rule.value) : String(rule.value);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function deserializeTree(query) {
|
|
291
|
+
if (query["filter[v]"] !== "2") return null;
|
|
292
|
+
const root = readTreeGroup("filter[root]", query);
|
|
293
|
+
if (!root) return null;
|
|
294
|
+
return { root };
|
|
295
|
+
}
|
|
296
|
+
function readTreeGroup(prefix, query) {
|
|
297
|
+
const combRaw = query[`${prefix}[combinator]`];
|
|
298
|
+
if (combRaw !== "and" && combRaw !== "or") return null;
|
|
299
|
+
const children = [];
|
|
300
|
+
for (let i = 0; i < 64; i++) {
|
|
301
|
+
const childPrefix = `${prefix}[children][${i}]`;
|
|
302
|
+
const type = query[`${childPrefix}[type]`];
|
|
303
|
+
if (type === "rule") {
|
|
304
|
+
const field = query[`${childPrefix}[field]`];
|
|
305
|
+
const op = query[`${childPrefix}[op]`];
|
|
306
|
+
if (typeof field !== "string" || typeof op !== "string" || !isValidOperator(op)) continue;
|
|
307
|
+
const rawVal = query[`${childPrefix}[value]`];
|
|
308
|
+
children.push({
|
|
309
|
+
id: crypto.randomUUID(),
|
|
310
|
+
type: "rule",
|
|
311
|
+
field,
|
|
312
|
+
operator: op,
|
|
313
|
+
value: parseSerializedFilterValue(rawVal)
|
|
314
|
+
});
|
|
315
|
+
} else if (type === "group") {
|
|
316
|
+
const sub = readTreeGroup(childPrefix, query);
|
|
317
|
+
if (sub) children.push(sub);
|
|
318
|
+
} else {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
id: crypto.randomUUID(),
|
|
324
|
+
type: "group",
|
|
325
|
+
combinator: combRaw,
|
|
326
|
+
children
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function isAdvancedFilterState(value) {
|
|
330
|
+
if (!value || typeof value !== "object") return false;
|
|
331
|
+
const record = value;
|
|
332
|
+
return Array.isArray(record.conditions) && (record.logic === "and" || record.logic === "or");
|
|
333
|
+
}
|
|
334
|
+
function flatToTree(flat) {
|
|
335
|
+
if (flat.conditions.length === 0) {
|
|
336
|
+
return { root: { id: crypto.randomUUID(), type: "group", combinator: "and", children: [] } };
|
|
337
|
+
}
|
|
338
|
+
const andRuns = [[flat.conditions[0]]];
|
|
339
|
+
for (let i = 1; i < flat.conditions.length; i++) {
|
|
340
|
+
const c = flat.conditions[i];
|
|
341
|
+
if (c.join === "or") andRuns.push([c]);
|
|
342
|
+
else andRuns[andRuns.length - 1].push(c);
|
|
343
|
+
}
|
|
344
|
+
const ruleFromCondition = (c) => ({
|
|
345
|
+
id: crypto.randomUUID(),
|
|
346
|
+
type: "rule",
|
|
347
|
+
field: c.field,
|
|
348
|
+
operator: c.operator,
|
|
349
|
+
value: c.value
|
|
350
|
+
});
|
|
351
|
+
const orChildren = andRuns.map((run) => {
|
|
352
|
+
if (run.length === 1) return ruleFromCondition(run[0]);
|
|
353
|
+
return {
|
|
354
|
+
id: crypto.randomUUID(),
|
|
355
|
+
type: "group",
|
|
356
|
+
combinator: "and",
|
|
357
|
+
children: run.map(ruleFromCondition)
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
if (orChildren.length === 1) {
|
|
361
|
+
const only = orChildren[0];
|
|
362
|
+
if (only.type === "group") return { root: only };
|
|
363
|
+
return { root: { id: crypto.randomUUID(), type: "group", combinator: "and", children: [only] } };
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
root: { id: crypto.randomUUID(), type: "group", combinator: "or", children: orChildren }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function mapDictionaryColorToTone(color) {
|
|
370
|
+
if (!color || typeof color !== "string") return void 0;
|
|
371
|
+
const trimmed = color.trim().toLowerCase();
|
|
372
|
+
if (!trimmed) return void 0;
|
|
373
|
+
const named = {
|
|
374
|
+
red: "error",
|
|
375
|
+
crimson: "error",
|
|
376
|
+
pink: "pink",
|
|
377
|
+
rose: "pink",
|
|
378
|
+
fuchsia: "pink",
|
|
379
|
+
magenta: "pink",
|
|
380
|
+
green: "success",
|
|
381
|
+
emerald: "success",
|
|
382
|
+
lime: "success",
|
|
383
|
+
teal: "success",
|
|
384
|
+
amber: "warning",
|
|
385
|
+
yellow: "warning",
|
|
386
|
+
orange: "warning",
|
|
387
|
+
blue: "info",
|
|
388
|
+
cyan: "info",
|
|
389
|
+
sky: "info",
|
|
390
|
+
indigo: "info",
|
|
391
|
+
gray: "neutral",
|
|
392
|
+
grey: "neutral",
|
|
393
|
+
slate: "neutral",
|
|
394
|
+
zinc: "neutral",
|
|
395
|
+
stone: "neutral",
|
|
396
|
+
violet: "brand",
|
|
397
|
+
purple: "brand"
|
|
398
|
+
};
|
|
399
|
+
if (named[trimmed]) return named[trimmed];
|
|
400
|
+
const hexMatch = /^#?([0-9a-f]{6})$/i.exec(trimmed);
|
|
401
|
+
if (!hexMatch) return void 0;
|
|
402
|
+
const hex = hexMatch[1];
|
|
403
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
404
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
405
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
406
|
+
const max = Math.max(r, g, b);
|
|
407
|
+
const min = Math.min(r, g, b);
|
|
408
|
+
const delta = max - min;
|
|
409
|
+
if (delta < 24) return "neutral";
|
|
410
|
+
let hue = 0;
|
|
411
|
+
if (max === r) hue = (g - b) / delta % 6;
|
|
412
|
+
else if (max === g) hue = (b - r) / delta + 2;
|
|
413
|
+
else hue = (r - g) / delta + 4;
|
|
414
|
+
hue = (hue * 60 + 360) % 360;
|
|
415
|
+
if (hue < 20) return "error";
|
|
416
|
+
if (hue < 50) return "warning";
|
|
417
|
+
if (hue < 80) return "warning";
|
|
418
|
+
if (hue < 170) return "success";
|
|
419
|
+
if (hue < 260) return "info";
|
|
420
|
+
if (hue < 320) return "brand";
|
|
421
|
+
if (hue < 340) return "pink";
|
|
422
|
+
return "error";
|
|
423
|
+
}
|
|
262
424
|
export {
|
|
263
425
|
OPERATORS_BY_FIELD_TYPE,
|
|
264
426
|
convertAdvancedFilterToWhere,
|
|
265
427
|
createEmptyCondition,
|
|
266
428
|
deserializeAdvancedFilter,
|
|
429
|
+
deserializeTree,
|
|
430
|
+
flatToTree,
|
|
267
431
|
getDefaultOperator,
|
|
432
|
+
isAdvancedFilterState,
|
|
268
433
|
isValidOperator,
|
|
269
434
|
isValuelessOperator,
|
|
435
|
+
mapDictionaryColorToTone,
|
|
270
436
|
normalizeAdvancedFilterState,
|
|
271
|
-
serializeAdvancedFilter
|
|
437
|
+
serializeAdvancedFilter,
|
|
438
|
+
serializeTree
|
|
272
439
|
};
|
|
273
440
|
//# sourceMappingURL=advanced-filter.js.map
|