@platforma-sdk/model 1.54.9 → 1.54.13
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/dist/components/PlDataTable/state-migration.cjs.map +1 -1
- package/dist/components/PlDataTable/state-migration.js.map +1 -1
- package/dist/components/PlDataTable/table.cjs +27 -9
- package/dist/components/PlDataTable/table.cjs.map +1 -1
- package/dist/components/PlDataTable/table.d.ts.map +1 -1
- package/dist/components/PlDataTable/table.js +28 -10
- package/dist/components/PlDataTable/table.js.map +1 -1
- package/dist/components/PlDataTable/v5.d.ts +8 -5
- package/dist/components/PlDataTable/v5.d.ts.map +1 -1
- package/dist/filters/converters/filterToQuery.cjs +21 -15
- package/dist/filters/converters/filterToQuery.cjs.map +1 -1
- package/dist/filters/converters/filterToQuery.d.ts +1 -1
- package/dist/filters/converters/filterToQuery.d.ts.map +1 -1
- package/dist/filters/converters/filterToQuery.js +21 -15
- package/dist/filters/converters/filterToQuery.js.map +1 -1
- package/dist/filters/converters/filterUiToExpressionImpl.cjs +80 -100
- package/dist/filters/converters/filterUiToExpressionImpl.cjs.map +1 -1
- package/dist/filters/converters/filterUiToExpressionImpl.d.ts.map +1 -1
- package/dist/filters/converters/filterUiToExpressionImpl.js +81 -101
- package/dist/filters/converters/filterUiToExpressionImpl.js.map +1 -1
- package/dist/filters/distill.cjs +18 -18
- package/dist/filters/distill.cjs.map +1 -1
- package/dist/filters/distill.d.ts +3 -2
- package/dist/filters/distill.d.ts.map +1 -1
- package/dist/filters/distill.js +18 -18
- package/dist/filters/distill.js.map +1 -1
- package/dist/filters/traverse.cjs +53 -0
- package/dist/filters/traverse.cjs.map +1 -0
- package/dist/filters/traverse.d.ts +27 -0
- package/dist/filters/traverse.d.ts.map +1 -0
- package/dist/filters/traverse.js +50 -0
- package/dist/filters/traverse.js.map +1 -0
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/pframe_utils/querySpec.d.ts +1 -1
- package/dist/pframe_utils/querySpec.d.ts.map +1 -1
- package/dist/render/api.cjs +1 -1
- package/dist/render/api.cjs.map +1 -1
- package/dist/render/api.d.ts.map +1 -1
- package/dist/render/api.js +2 -2
- package/dist/render/api.js.map +1 -1
- package/package.json +6 -6
- package/src/components/PlDataTable/state-migration.ts +4 -4
- package/src/components/PlDataTable/table.ts +43 -12
- package/src/components/PlDataTable/v5.ts +10 -7
- package/src/filters/converters/filterToQuery.ts +25 -17
- package/src/filters/converters/filterUiToExpressionImpl.ts +81 -125
- package/src/filters/distill.ts +28 -24
- package/src/filters/traverse.test.ts +134 -0
- package/src/filters/traverse.ts +84 -0
- package/src/pframe_utils/querySpec.ts +1 -1
- package/src/render/api.ts +2 -2
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
SingleAxisSelector,
|
|
16
16
|
SpecQueryExpression,
|
|
17
17
|
SpecQueryJoinEntry,
|
|
18
|
+
CanonicalizedJson,
|
|
18
19
|
} from "@milaboratories/pl-model-common";
|
|
19
20
|
import {
|
|
20
21
|
Annotation,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
readAnnotation,
|
|
26
27
|
uniqueBy,
|
|
27
28
|
isBooleanExpression,
|
|
29
|
+
parseJson,
|
|
28
30
|
} from "@milaboratories/pl-model-common";
|
|
29
31
|
import { filterSpecToSpecQueryExpr } from "../../filters";
|
|
30
32
|
import type { RenderCtxBase, TreeNodeAccessor, PColumnDataUniversal } from "../../render";
|
|
@@ -36,6 +38,7 @@ import { upgradePlDataTableStateV2 } from "./state-migration";
|
|
|
36
38
|
import type { PlDataTableStateV2 } from "./state-migration";
|
|
37
39
|
import type { PlDataTableSheet } from "./v5";
|
|
38
40
|
import { getAllLabelColumns, getMatchingLabelColumns } from "./labels";
|
|
41
|
+
import { collectFilterSpecColumns } from "../../filters/traverse";
|
|
39
42
|
|
|
40
43
|
/** Convert a PTableColumnId to a SpecQueryExpression reference. */
|
|
41
44
|
function columnIdToExpr(col: PTableColumnId): SpecQueryExpression {
|
|
@@ -177,21 +180,37 @@ export function createPlDataTableV2<A, U>(
|
|
|
177
180
|
...fullColumns.map((c) => ({ type: "column", id: c.id }) satisfies PTableColumnIdColumn),
|
|
178
181
|
];
|
|
179
182
|
const fullColumnsIdsSet = new Set(fullColumnsIds.map((c) => canonicalizeJson<PTableColumnId>(c)));
|
|
180
|
-
const isValidColumnId = (id:
|
|
181
|
-
fullColumnsIdsSet.has(
|
|
183
|
+
const isValidColumnId = (id: string): boolean =>
|
|
184
|
+
fullColumnsIdsSet.has(id as CanonicalizedJson<PTableColumnId>);
|
|
185
|
+
|
|
186
|
+
// -- Filtering validation --
|
|
187
|
+
const stateFilters = tableStateNormalized.pTableParams.filters;
|
|
188
|
+
const opsFilters = ops?.filters ?? null;
|
|
189
|
+
const filters: null | PlDataTableFilters =
|
|
190
|
+
stateFilters != null && opsFilters != null
|
|
191
|
+
? { type: "and", filters: [stateFilters, opsFilters] }
|
|
192
|
+
: (stateFilters ?? opsFilters);
|
|
193
|
+
const filterColumns = filters ? collectFilterSpecColumns(filters) : [];
|
|
194
|
+
const firstInvalidFilterColumn = filterColumns.find((col) => !isValidColumnId(col));
|
|
195
|
+
if (firstInvalidFilterColumn)
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Invalid filter column ${firstInvalidFilterColumn}: column reference does not match the table columns`,
|
|
198
|
+
);
|
|
182
199
|
|
|
183
|
-
|
|
184
|
-
const filters = tableStateNormalized.pTableParams.filters;
|
|
200
|
+
// -- Sorting validation --
|
|
185
201
|
const sorting: PTableSorting[] = uniqueBy(
|
|
186
202
|
[...tableStateNormalized.pTableParams.sorting, ...(ops?.sorting ?? [])],
|
|
187
203
|
(s) => canonicalizeJson<PTableColumnId>(s.column),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
);
|
|
205
|
+
const firstInvalidSortingColumn = sorting.find(
|
|
206
|
+
(s) => !isValidColumnId(canonicalizeJson<PTableColumnId>(s.column)),
|
|
207
|
+
);
|
|
208
|
+
if (firstInvalidSortingColumn)
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Invalid sorting column ${JSON.stringify(firstInvalidSortingColumn.column)}: column reference does not match the table columns`,
|
|
211
|
+
);
|
|
194
212
|
|
|
213
|
+
const coreJoinType = ops?.coreJoinType ?? "full";
|
|
195
214
|
const fullDef = createPTableDef({
|
|
196
215
|
columns,
|
|
197
216
|
labelColumns: fullLabelColumns,
|
|
@@ -200,6 +219,7 @@ export function createPlDataTableV2<A, U>(
|
|
|
200
219
|
sorting,
|
|
201
220
|
coreColumnPredicate: ops?.coreColumnPredicate,
|
|
202
221
|
});
|
|
222
|
+
|
|
203
223
|
const fullHandle = ctx.createPTableV2(fullDef);
|
|
204
224
|
if (!fullHandle) return undefined;
|
|
205
225
|
|
|
@@ -227,12 +247,22 @@ export function createPlDataTableV2<A, U>(
|
|
|
227
247
|
coreColumns.forEach((c) => hiddenColumns.delete(c));
|
|
228
248
|
}
|
|
229
249
|
|
|
230
|
-
//
|
|
250
|
+
// Preserve sorted columns from being hidden
|
|
231
251
|
sorting
|
|
232
252
|
.map((s) => s.column)
|
|
233
253
|
.filter((c): c is PTableColumnIdColumn => c.type === "column")
|
|
234
254
|
.forEach((c) => hiddenColumns.delete(c.id));
|
|
235
255
|
|
|
256
|
+
// Preserve filter columns from being hidden
|
|
257
|
+
if (filters) {
|
|
258
|
+
collectFilterSpecColumns(filters)
|
|
259
|
+
.flatMap((c) => {
|
|
260
|
+
const obj = parseJson(c);
|
|
261
|
+
return obj.type === "column" ? [obj.id] : [];
|
|
262
|
+
})
|
|
263
|
+
.forEach((c) => hiddenColumns.delete(c));
|
|
264
|
+
}
|
|
265
|
+
|
|
236
266
|
const visibleColumns = columns.filter((c) => !hiddenColumns.has(c.id));
|
|
237
267
|
const visibleLabelColumns = getMatchingLabelColumns(
|
|
238
268
|
visibleColumns.map(getColumnIdAndSpec),
|
|
@@ -251,13 +281,14 @@ export function createPlDataTableV2<A, U>(
|
|
|
251
281
|
coreColumnPredicate,
|
|
252
282
|
});
|
|
253
283
|
const visibleHandle = ctx.createPTableV2(visibleDef);
|
|
284
|
+
|
|
254
285
|
if (!visibleHandle) return undefined;
|
|
255
286
|
|
|
256
287
|
return {
|
|
257
288
|
sourceId: tableStateNormalized.pTableParams.sourceId,
|
|
258
289
|
fullTableHandle: fullHandle,
|
|
259
290
|
visibleTableHandle: visibleHandle,
|
|
260
|
-
}
|
|
291
|
+
} as PlDataTableModel;
|
|
261
292
|
}
|
|
262
293
|
|
|
263
294
|
/** Create sheet entries for PlDataTable */
|
|
@@ -5,12 +5,13 @@ import type {
|
|
|
5
5
|
ListOptionBase,
|
|
6
6
|
PObjectId,
|
|
7
7
|
PTableColumnSpec,
|
|
8
|
-
PTableRecordFilter,
|
|
9
8
|
PTableSorting,
|
|
10
9
|
PColumnIdAndSpec,
|
|
11
10
|
PTableHandle,
|
|
11
|
+
RootFilterSpec,
|
|
12
|
+
PTableColumnId,
|
|
12
13
|
} from "@milaboratories/pl-model-common";
|
|
13
|
-
import type {
|
|
14
|
+
import type { FilterSpecLeaf } from "../../filters";
|
|
14
15
|
|
|
15
16
|
export type PlTableColumnId = {
|
|
16
17
|
/** Original column spec */
|
|
@@ -61,10 +62,10 @@ export type PlDataTableSheetState = {
|
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
/** Tree-based filter state compatible with PlAdvancedFilter's RootFilter */
|
|
64
|
-
export type PlDataTableFilters =
|
|
65
|
-
export type PlDataTableFiltersWithMeta =
|
|
66
|
-
FilterSpecLeaf<
|
|
67
|
-
{ id: number; isExpanded?: boolean }
|
|
65
|
+
export type PlDataTableFilters = RootFilterSpec<FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>>;
|
|
66
|
+
export type PlDataTableFiltersWithMeta = RootFilterSpec<
|
|
67
|
+
FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>,
|
|
68
|
+
{ id: number; isExpanded?: boolean; source?: "table-filter" | "table-search" }
|
|
68
69
|
>;
|
|
69
70
|
|
|
70
71
|
export type PlDataTableStateV2CacheEntry = {
|
|
@@ -76,6 +77,8 @@ export type PlDataTableStateV2CacheEntry = {
|
|
|
76
77
|
sheetsState: PlDataTableSheetState[];
|
|
77
78
|
/** Filters state (tree-based, compatible with PlAdvancedFilter) */
|
|
78
79
|
filtersState: null | PlDataTableFiltersWithMeta;
|
|
80
|
+
/** Fast search string */
|
|
81
|
+
searchString?: string;
|
|
79
82
|
};
|
|
80
83
|
|
|
81
84
|
export type PTableParamsV2 =
|
|
@@ -113,7 +116,7 @@ export type PlDataTableModel = {
|
|
|
113
116
|
|
|
114
117
|
export type CreatePlDataTableOps = {
|
|
115
118
|
/** Filters for columns and non-partitioned axes */
|
|
116
|
-
filters?:
|
|
119
|
+
filters?: PlDataTableFilters;
|
|
117
120
|
|
|
118
121
|
/** Sorting to columns hidden from user */
|
|
119
122
|
sorting?: PTableSorting[];
|
|
@@ -6,34 +6,42 @@ import type {
|
|
|
6
6
|
SingleAxisSelector,
|
|
7
7
|
SpecQueryExpression,
|
|
8
8
|
} from "@milaboratories/pl-model-common";
|
|
9
|
+
import { traverseFilterSpec } from "../traverse";
|
|
9
10
|
|
|
10
11
|
/** Parses a CanonicalizedJson<PTableColumnId> string into a SpecQueryExpression reference. */
|
|
11
12
|
function resolveColumnRef(columnStr: string): SpecQueryExpression {
|
|
12
13
|
const parsed = JSON.parse(columnStr) as PTableColumnId;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return { type: "columnRef", value: parsed.id };
|
|
14
|
+
return parsed.type === "axis"
|
|
15
|
+
? { type: "axisRef", value: parsed.id as SingleAxisSelector }
|
|
16
|
+
: { type: "columnRef", value: parsed.id };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Converts a FilterSpec tree into a SpecQueryExpression. */
|
|
20
|
-
export function filterSpecToSpecQueryExpr(
|
|
21
|
-
filter: FilterSpec<
|
|
20
|
+
export function filterSpecToSpecQueryExpr<Leaf extends FilterSpecLeaf<string>>(
|
|
21
|
+
filter: FilterSpec<Leaf>,
|
|
22
22
|
): SpecQueryExpression {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const inputs = filter.filters
|
|
27
|
-
.filter((f) => f.type !== undefined)
|
|
28
|
-
.map(filterSpecToSpecQueryExpr);
|
|
23
|
+
return traverseFilterSpec(filter, {
|
|
24
|
+
leaf: leafToSpecQueryExpr,
|
|
25
|
+
and: (inputs) => {
|
|
29
26
|
if (inputs.length === 0) {
|
|
30
|
-
throw new Error(
|
|
27
|
+
throw new Error("AND filter requires at least one operand");
|
|
31
28
|
}
|
|
32
|
-
return { type:
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
return { type: "and", input: inputs };
|
|
30
|
+
},
|
|
31
|
+
or: (inputs) => {
|
|
32
|
+
if (inputs.length === 0) {
|
|
33
|
+
throw new Error("OR filter requires at least one operand");
|
|
34
|
+
}
|
|
35
|
+
return { type: "or", input: inputs };
|
|
36
|
+
},
|
|
37
|
+
not: (input) => ({ type: "not", input }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
36
40
|
|
|
41
|
+
function leafToSpecQueryExpr<Leaf extends FilterSpecLeaf<string>>(
|
|
42
|
+
filter: Leaf,
|
|
43
|
+
): SpecQueryExpression {
|
|
44
|
+
switch (filter.type) {
|
|
37
45
|
case "patternEquals":
|
|
38
46
|
return {
|
|
39
47
|
type: "stringEquals",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertNever } from "@milaboratories/pl-model-common";
|
|
2
|
+
import type { FilterSpecLeaf } from "@milaboratories/pl-model-common";
|
|
2
3
|
import {
|
|
3
4
|
and,
|
|
4
5
|
col,
|
|
@@ -9,135 +10,90 @@ import {
|
|
|
9
10
|
type ExpressionImpl,
|
|
10
11
|
} from "@milaboratories/ptabler-expression-js";
|
|
11
12
|
import type { FilterSpec } from "../types";
|
|
13
|
+
import { traverseFilterSpec } from "../traverse";
|
|
12
14
|
|
|
13
15
|
export function convertFilterUiToExpressionImpl(value: FilterSpec): ExpressionImpl {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return and(...expressions);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (value.type === "not") {
|
|
35
|
-
return convertFilterUiToExpressionImpl(value.filter).not();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (value.type === "isNA") {
|
|
39
|
-
return col(value.column).isNull();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (value.type === "isNotNA") {
|
|
43
|
-
return col(value.column).isNotNull();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (value.type === "patternEquals") {
|
|
47
|
-
return col(value.column).eq(lit(value.value));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (value.type === "patternNotEquals") {
|
|
51
|
-
return col(value.column).neq(lit(value.value));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (value.type === "patternContainSubsequence") {
|
|
55
|
-
return col(value.column).strContains(value.value, false, true);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (value.type === "patternNotContainSubsequence") {
|
|
59
|
-
return col(value.column).strContains(value.value, false, true).not();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (value.type === "equal") {
|
|
63
|
-
return col(value.column).eq(lit(value.x));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (value.type === "notEqual") {
|
|
67
|
-
return col(value.column).neq(lit(value.x));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (value.type === "lessThan") {
|
|
71
|
-
return col(value.column).lt(lit(value.x));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (value.type === "greaterThan") {
|
|
75
|
-
return col(value.column).gt(lit(value.x));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (value.type === "lessThanOrEqual") {
|
|
79
|
-
return col(value.column).le(lit(value.x));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (value.type === "greaterThanOrEqual") {
|
|
83
|
-
return col(value.column).ge(lit(value.x));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (value.type === "equalToColumn") {
|
|
87
|
-
return col(value.column).eq(col(value.rhs));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (value.type === "greaterThanColumn") {
|
|
91
|
-
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
92
|
-
return col(value.column).plus(lit(value.minDiff)).gt(col(value.rhs));
|
|
93
|
-
}
|
|
94
|
-
return col(value.column).gt(col(value.rhs));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (value.type === "lessThanColumn") {
|
|
98
|
-
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
99
|
-
return col(value.column).plus(lit(value.minDiff)).lt(col(value.rhs));
|
|
100
|
-
}
|
|
101
|
-
return col(value.column).lt(col(value.rhs));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (value.type === "greaterThanColumnOrEqual") {
|
|
105
|
-
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
106
|
-
return col(value.column).plus(lit(value.minDiff)).ge(col(value.rhs));
|
|
107
|
-
}
|
|
108
|
-
return col(value.column).ge(col(value.rhs));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (value.type === "lessThanColumnOrEqual") {
|
|
112
|
-
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
113
|
-
return col(value.column).plus(lit(value.minDiff)).le(col(value.rhs));
|
|
114
|
-
}
|
|
115
|
-
return col(value.column).le(col(value.rhs));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (value.type === "topN") {
|
|
119
|
-
return rank(col(value.column), true).over([]).le(lit(value.n));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (value.type === "bottomN") {
|
|
123
|
-
return rank(col(value.column), false).over([]).le(lit(value.n));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
value.type === "patternMatchesRegularExpression" ||
|
|
128
|
-
value.type === "patternFuzzyContainSubsequence" ||
|
|
129
|
-
value.type === "inSet" ||
|
|
130
|
-
value.type === "notInSet" ||
|
|
131
|
-
value.type === "ifNa"
|
|
132
|
-
) {
|
|
133
|
-
throw new Error("Not implemented filter type: " + value.type);
|
|
134
|
-
}
|
|
16
|
+
return traverseFilterSpec(value, {
|
|
17
|
+
leaf: leafToExpressionImpl,
|
|
18
|
+
and: (expressions) => {
|
|
19
|
+
if (expressions.length === 0) {
|
|
20
|
+
throw new Error("AND filter requires at least one operand");
|
|
21
|
+
}
|
|
22
|
+
return and(...expressions);
|
|
23
|
+
},
|
|
24
|
+
or: (expressions) => {
|
|
25
|
+
if (expressions.length === 0) {
|
|
26
|
+
throw new Error("OR filter requires at least one operand");
|
|
27
|
+
}
|
|
28
|
+
return or(...expressions);
|
|
29
|
+
},
|
|
30
|
+
not: (result) => result.not(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
135
33
|
|
|
136
|
-
|
|
137
|
-
|
|
34
|
+
function leafToExpressionImpl(value: FilterSpecLeaf): ExpressionImpl {
|
|
35
|
+
switch (value.type) {
|
|
36
|
+
case "isNA":
|
|
37
|
+
return col(value.column).isNull();
|
|
38
|
+
case "isNotNA":
|
|
39
|
+
return col(value.column).isNotNull();
|
|
40
|
+
case "patternEquals":
|
|
41
|
+
return col(value.column).eq(lit(value.value));
|
|
42
|
+
case "patternNotEquals":
|
|
43
|
+
return col(value.column).neq(lit(value.value));
|
|
44
|
+
case "patternContainSubsequence":
|
|
45
|
+
return col(value.column).strContains(value.value, false, true);
|
|
46
|
+
case "patternNotContainSubsequence":
|
|
47
|
+
return col(value.column).strContains(value.value, false, true).not();
|
|
48
|
+
case "equal":
|
|
49
|
+
return col(value.column).eq(lit(value.x));
|
|
50
|
+
case "notEqual":
|
|
51
|
+
return col(value.column).neq(lit(value.x));
|
|
52
|
+
case "lessThan":
|
|
53
|
+
return col(value.column).lt(lit(value.x));
|
|
54
|
+
case "greaterThan":
|
|
55
|
+
return col(value.column).gt(lit(value.x));
|
|
56
|
+
case "lessThanOrEqual":
|
|
57
|
+
return col(value.column).le(lit(value.x));
|
|
58
|
+
case "greaterThanOrEqual":
|
|
59
|
+
return col(value.column).ge(lit(value.x));
|
|
60
|
+
case "equalToColumn":
|
|
61
|
+
return col(value.column).eq(col(value.rhs));
|
|
62
|
+
case "greaterThanColumn":
|
|
63
|
+
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
64
|
+
return col(value.column).plus(lit(value.minDiff)).gt(col(value.rhs));
|
|
65
|
+
}
|
|
66
|
+
return col(value.column).gt(col(value.rhs));
|
|
67
|
+
case "lessThanColumn":
|
|
68
|
+
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
69
|
+
return col(value.column).plus(lit(value.minDiff)).lt(col(value.rhs));
|
|
70
|
+
}
|
|
71
|
+
return col(value.column).lt(col(value.rhs));
|
|
72
|
+
case "greaterThanColumnOrEqual":
|
|
73
|
+
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
74
|
+
return col(value.column).plus(lit(value.minDiff)).ge(col(value.rhs));
|
|
75
|
+
}
|
|
76
|
+
return col(value.column).ge(col(value.rhs));
|
|
77
|
+
case "lessThanColumnOrEqual":
|
|
78
|
+
if (value.minDiff !== undefined && value.minDiff !== 0) {
|
|
79
|
+
return col(value.column).plus(lit(value.minDiff)).le(col(value.rhs));
|
|
80
|
+
}
|
|
81
|
+
return col(value.column).le(col(value.rhs));
|
|
82
|
+
case "topN":
|
|
83
|
+
return rank(col(value.column), true).over([]).le(lit(value.n));
|
|
84
|
+
case "bottomN":
|
|
85
|
+
return rank(col(value.column), false).over([]).le(lit(value.n));
|
|
86
|
+
case "patternMatchesRegularExpression":
|
|
87
|
+
case "patternFuzzyContainSubsequence":
|
|
88
|
+
case "inSet":
|
|
89
|
+
case "notInSet":
|
|
90
|
+
case "ifNa":
|
|
91
|
+
throw new Error("Not implemented filter type: " + value.type);
|
|
92
|
+
case undefined:
|
|
93
|
+
throw new Error("Filter type is undefined, this should not happen");
|
|
94
|
+
default:
|
|
95
|
+
assertNever(value);
|
|
138
96
|
}
|
|
139
|
-
|
|
140
|
-
assertNever(value);
|
|
141
97
|
}
|
|
142
98
|
|
|
143
99
|
export function convertFilterUiToExpressions(value: FilterSpec): Expression {
|
package/src/filters/distill.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { DistributiveKeys, UnionToTuples } from "@milaboratories/helpers";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
RootFilterSpec,
|
|
4
|
+
type FilterSpec,
|
|
5
|
+
type FilterSpecLeaf,
|
|
6
|
+
} from "@milaboratories/pl-model-common";
|
|
7
|
+
import { traverseFilterSpec } from "./traverse";
|
|
8
|
+
import { InferFilterSpecLeaf } from "@milaboratories/pl-model-common";
|
|
3
9
|
|
|
4
10
|
/** All possible field names that can appear in any FilterSpecLeaf variant. */
|
|
5
11
|
type FilterSpecLeafKey = DistributiveKeys<FilterSpecLeaf<string>>;
|
|
@@ -36,32 +42,30 @@ function distillLeaf(node: Record<string, unknown>): FilterSpecLeaf<string> {
|
|
|
36
42
|
return result as FilterSpecLeaf<string>;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
function distillNode<T extends FilterSpecLeaf<unknown>>(
|
|
40
|
-
node: FilterSpec<T, unknown, unknown>,
|
|
41
|
-
): FilterSpec<T> | null {
|
|
42
|
-
switch (node.type) {
|
|
43
|
-
case "and":
|
|
44
|
-
case "or": {
|
|
45
|
-
const filtered = node.filters.map(distillNode).filter((f): f is FilterSpec<T> => f !== null);
|
|
46
|
-
return filtered.length === 0 ? null : { type: node.type, filters: filtered };
|
|
47
|
-
}
|
|
48
|
-
case "not": {
|
|
49
|
-
const inner = distillNode(node.filter);
|
|
50
|
-
return inner === null ? null : { type: "not", filter: inner };
|
|
51
|
-
}
|
|
52
|
-
default:
|
|
53
|
-
if (!isFilledLeaf(node)) return null;
|
|
54
|
-
return distillLeaf(node);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
45
|
/**
|
|
59
46
|
* Strips non-FilterSpec metadata (whitelist approach) and removes
|
|
60
47
|
* unfilled leaves (type is undefined or any required field is undefined).
|
|
61
48
|
*/
|
|
62
|
-
export function distillFilterSpec<
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
export function distillFilterSpec<
|
|
50
|
+
FS extends FilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>,
|
|
51
|
+
R extends FS extends RootFilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>
|
|
52
|
+
? RootFilterSpec<InferFilterSpecLeaf<FS>>
|
|
53
|
+
: FilterSpec<InferFilterSpecLeaf<FS>>,
|
|
54
|
+
>(filter: null | undefined | FS): null | R {
|
|
65
55
|
if (filter == null) return null;
|
|
66
|
-
return
|
|
56
|
+
return traverseFilterSpec<FS, null | R>(filter, {
|
|
57
|
+
leaf: (leaf) => {
|
|
58
|
+
if (!isFilledLeaf(leaf as Record<string, unknown>)) return null;
|
|
59
|
+
return distillLeaf(leaf as Record<string, unknown>) as R;
|
|
60
|
+
},
|
|
61
|
+
and: (results) => {
|
|
62
|
+
const filtered = results.filter((f): f is NonNullable<typeof f> => f !== null);
|
|
63
|
+
return filtered.length === 0 ? null : ({ type: "and", filters: filtered } as R);
|
|
64
|
+
},
|
|
65
|
+
or: (results) => {
|
|
66
|
+
const filtered = results.filter((f): f is NonNullable<typeof f> => f !== null);
|
|
67
|
+
return filtered.length === 0 ? null : ({ type: "or", filters: filtered } as R);
|
|
68
|
+
},
|
|
69
|
+
not: (result) => (result === null ? null : ({ type: "not", filter: result } as R)),
|
|
70
|
+
});
|
|
67
71
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { FilterSpec, FilterSpecLeaf } from "@milaboratories/pl-model-common";
|
|
3
|
+
import { traverseFilterSpec, collectFilterSpecColumns } from "./traverse";
|
|
4
|
+
|
|
5
|
+
type F = FilterSpec<FilterSpecLeaf<string>>;
|
|
6
|
+
|
|
7
|
+
const eq1: F = { type: "equal", column: "c1", x: 1 };
|
|
8
|
+
const eq2: F = { type: "equal", column: "c2", x: 2 };
|
|
9
|
+
const gt: F = { type: "greaterThan", column: "c3", x: 10 };
|
|
10
|
+
const empty: F = { type: undefined };
|
|
11
|
+
|
|
12
|
+
/** Visitor that collects leaf types into a string expression. */
|
|
13
|
+
const toStringVisitor = {
|
|
14
|
+
leaf: (l: FilterSpecLeaf<unknown>) => l.type ?? "empty",
|
|
15
|
+
and: (r: string[]) => `(${r.join(" & ")})`,
|
|
16
|
+
or: (r: string[]) => `(${r.join(" | ")})`,
|
|
17
|
+
not: (r: string) => `!${r}`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("traverseFilterSpec", () => {
|
|
21
|
+
it("visits a leaf node", () => {
|
|
22
|
+
expect(traverseFilterSpec(eq1, toStringVisitor)).toBe("equal");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("visits an and node", () => {
|
|
26
|
+
const filter: F = { type: "and", filters: [eq1, gt] };
|
|
27
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("(equal & greaterThan)");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("visits an or node", () => {
|
|
31
|
+
const filter: F = { type: "or", filters: [eq1, eq2] };
|
|
32
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("(equal | equal)");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("visits a not node", () => {
|
|
36
|
+
const filter: F = { type: "not", filter: eq1 };
|
|
37
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("!equal");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("handles nested structure", () => {
|
|
41
|
+
const filter: F = {
|
|
42
|
+
type: "and",
|
|
43
|
+
filters: [
|
|
44
|
+
{ type: "not", filter: eq1 },
|
|
45
|
+
{ type: "or", filters: [eq2, gt] },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("(!equal & (equal | greaterThan))");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("skips { type: undefined } entries in and", () => {
|
|
52
|
+
const filter: F = { type: "and", filters: [empty, eq1, empty] };
|
|
53
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("(equal)");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("skips { type: undefined } entries in or", () => {
|
|
57
|
+
const filter: F = { type: "or", filters: [empty, eq1, empty, eq2] };
|
|
58
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("(equal | equal)");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns empty and when all children are undefined", () => {
|
|
62
|
+
const filter: F = { type: "and", filters: [empty, empty] };
|
|
63
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("()");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns empty or when all children are undefined", () => {
|
|
67
|
+
const filter: F = { type: "or", filters: [empty] };
|
|
68
|
+
expect(traverseFilterSpec(filter, toStringVisitor)).toBe("()");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("works with CommonNode/CommonLeaf metadata", () => {
|
|
72
|
+
type MetaLeaf = FilterSpecLeaf<string>;
|
|
73
|
+
type MetaFilter = FilterSpec<MetaLeaf, { _id: number }, { _id: number }>;
|
|
74
|
+
|
|
75
|
+
const filter: MetaFilter = {
|
|
76
|
+
type: "and",
|
|
77
|
+
_id: 1,
|
|
78
|
+
filters: [
|
|
79
|
+
{ type: "equal", column: "c1", x: 1, _id: 2 },
|
|
80
|
+
{ type: "greaterThan", column: "c2", x: 5, _id: 3 },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = traverseFilterSpec(filter, {
|
|
85
|
+
leaf: (l) => [l._id],
|
|
86
|
+
and: (r) => r.flat(),
|
|
87
|
+
or: (r) => r.flat(),
|
|
88
|
+
not: (r) => r,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual([2, 3]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("collectFilterSpecColumns", () => {
|
|
96
|
+
it("collects column from a single leaf", () => {
|
|
97
|
+
expect(collectFilterSpecColumns(eq1)).toEqual(["c1"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("collects columns from and", () => {
|
|
101
|
+
const filter: F = { type: "and", filters: [eq1, eq2, gt] };
|
|
102
|
+
expect(collectFilterSpecColumns(filter)).toEqual(["c1", "c2", "c3"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("collects columns from nested structure", () => {
|
|
106
|
+
const filter: F = {
|
|
107
|
+
type: "or",
|
|
108
|
+
filters: [
|
|
109
|
+
{ type: "not", filter: eq1 },
|
|
110
|
+
{ type: "and", filters: [eq2, gt] },
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
expect(collectFilterSpecColumns(filter)).toEqual(["c1", "c2", "c3"]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("collects rhs fields", () => {
|
|
117
|
+
const filter: F = { type: "greaterThanColumn", column: "c1", rhs: "c2" };
|
|
118
|
+
expect(collectFilterSpecColumns(filter)).toEqual(["c1", "c2"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("skips { type: undefined } entries", () => {
|
|
122
|
+
const filter: F = { type: "and", filters: [empty, eq1, empty] };
|
|
123
|
+
expect(collectFilterSpecColumns(filter)).toEqual(["c1"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns empty array for all-empty and", () => {
|
|
127
|
+
const filter: F = { type: "and", filters: [empty, empty] };
|
|
128
|
+
expect(collectFilterSpecColumns(filter)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns empty array for empty leaf", () => {
|
|
132
|
+
expect(collectFilterSpecColumns(empty)).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|