@platforma-sdk/model 1.53.15 → 1.54.7

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.
Files changed (142) hide show
  1. package/dist/annotations/converter.cjs +8 -32
  2. package/dist/annotations/converter.cjs.map +1 -1
  3. package/dist/annotations/converter.d.ts.map +1 -1
  4. package/dist/annotations/converter.js +7 -31
  5. package/dist/annotations/converter.js.map +1 -1
  6. package/dist/block_model.cjs +1 -0
  7. package/dist/block_model.cjs.map +1 -1
  8. package/dist/block_model.d.ts.map +1 -1
  9. package/dist/block_model.js +1 -0
  10. package/dist/block_model.js.map +1 -1
  11. package/dist/builder.cjs +1 -0
  12. package/dist/builder.cjs.map +1 -1
  13. package/dist/builder.d.ts.map +1 -1
  14. package/dist/builder.js +1 -0
  15. package/dist/builder.js.map +1 -1
  16. package/dist/components/PlDataTable/index.d.ts +5 -0
  17. package/dist/components/PlDataTable/index.d.ts.map +1 -0
  18. package/dist/components/PlDataTable/labels.cjs +91 -0
  19. package/dist/components/PlDataTable/labels.cjs.map +1 -0
  20. package/dist/components/PlDataTable/labels.d.ts +7 -0
  21. package/dist/components/PlDataTable/labels.d.ts.map +1 -0
  22. package/dist/components/PlDataTable/labels.js +88 -0
  23. package/dist/components/PlDataTable/labels.js.map +1 -0
  24. package/dist/components/PlDataTable/state-migration.cjs +162 -0
  25. package/dist/components/PlDataTable/state-migration.cjs.map +1 -0
  26. package/dist/components/PlDataTable/state-migration.d.ts +94 -0
  27. package/dist/components/PlDataTable/state-migration.d.ts.map +1 -0
  28. package/dist/components/PlDataTable/state-migration.js +158 -0
  29. package/dist/components/PlDataTable/state-migration.js.map +1 -0
  30. package/dist/components/PlDataTable/table.cjs +188 -0
  31. package/dist/components/PlDataTable/table.cjs.map +1 -0
  32. package/dist/components/PlDataTable/table.d.ts +26 -0
  33. package/dist/components/PlDataTable/table.d.ts.map +1 -0
  34. package/dist/components/PlDataTable/table.js +184 -0
  35. package/dist/components/PlDataTable/table.js.map +1 -0
  36. package/dist/components/PlDataTable/v4.d.ts +157 -0
  37. package/dist/components/PlDataTable/v4.d.ts.map +1 -0
  38. package/dist/components/PlDataTable/v5.d.ts +113 -0
  39. package/dist/components/PlDataTable/v5.d.ts.map +1 -0
  40. package/dist/filters/converters/filterEmpty.cjs +49 -0
  41. package/dist/filters/converters/filterEmpty.cjs.map +1 -0
  42. package/dist/filters/converters/filterEmpty.d.ts +4 -0
  43. package/dist/filters/converters/filterEmpty.d.ts.map +1 -0
  44. package/dist/filters/converters/filterEmpty.js +46 -0
  45. package/dist/filters/converters/filterEmpty.js.map +1 -0
  46. package/dist/filters/converters/filterToQuery.cjs +244 -0
  47. package/dist/filters/converters/filterToQuery.cjs.map +1 -0
  48. package/dist/filters/converters/filterToQuery.d.ts +4 -0
  49. package/dist/filters/converters/filterToQuery.d.ts.map +1 -0
  50. package/dist/filters/converters/filterToQuery.js +242 -0
  51. package/dist/filters/converters/filterToQuery.js.map +1 -0
  52. package/dist/filters/{converter.cjs → converters/filterUiToExpressionImpl.cjs} +3 -2
  53. package/dist/filters/converters/filterUiToExpressionImpl.cjs.map +1 -0
  54. package/dist/filters/{converter.d.ts → converters/filterUiToExpressionImpl.d.ts} +2 -2
  55. package/dist/filters/converters/filterUiToExpressionImpl.d.ts.map +1 -0
  56. package/dist/filters/{converter.js → converters/filterUiToExpressionImpl.js} +3 -2
  57. package/dist/filters/converters/filterUiToExpressionImpl.js.map +1 -0
  58. package/dist/filters/converters/index.d.ts +3 -0
  59. package/dist/filters/converters/index.d.ts.map +1 -0
  60. package/dist/filters/distill.cjs +46 -0
  61. package/dist/filters/distill.cjs.map +1 -0
  62. package/dist/filters/distill.d.ts +6 -0
  63. package/dist/filters/distill.d.ts.map +1 -0
  64. package/dist/filters/distill.js +44 -0
  65. package/dist/filters/distill.js.map +1 -0
  66. package/dist/filters/index.d.ts +2 -1
  67. package/dist/filters/index.d.ts.map +1 -1
  68. package/dist/filters/types.d.ts +1 -117
  69. package/dist/filters/types.d.ts.map +1 -1
  70. package/dist/index.cjs +17 -15
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.js +5 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/package.json.cjs +1 -1
  75. package/dist/package.json.js +1 -1
  76. package/dist/pframe_utils/columns.cjs +2 -0
  77. package/dist/pframe_utils/columns.cjs.map +1 -1
  78. package/dist/pframe_utils/columns.js +2 -0
  79. package/dist/pframe_utils/columns.js.map +1 -1
  80. package/dist/pframe_utils/index.cjs +2 -0
  81. package/dist/pframe_utils/index.cjs.map +1 -1
  82. package/dist/pframe_utils/index.js +2 -0
  83. package/dist/pframe_utils/index.js.map +1 -1
  84. package/dist/pframe_utils/querySpec.d.ts +2 -0
  85. package/dist/pframe_utils/querySpec.d.ts.map +1 -0
  86. package/dist/render/api.cjs +7 -0
  87. package/dist/render/api.cjs.map +1 -1
  88. package/dist/render/api.d.ts +3 -2
  89. package/dist/render/api.d.ts.map +1 -1
  90. package/dist/render/api.js +8 -1
  91. package/dist/render/api.js.map +1 -1
  92. package/dist/render/future.d.ts +1 -1
  93. package/dist/render/index.d.ts +2 -1
  94. package/dist/render/index.d.ts.map +1 -1
  95. package/dist/render/internal.cjs.map +1 -1
  96. package/dist/render/internal.d.ts +4 -1
  97. package/dist/render/internal.d.ts.map +1 -1
  98. package/dist/render/internal.js.map +1 -1
  99. package/dist/render/util/column_collection.cjs.map +1 -1
  100. package/dist/render/util/column_collection.d.ts +1 -1
  101. package/dist/render/util/column_collection.d.ts.map +1 -1
  102. package/dist/render/util/column_collection.js.map +1 -1
  103. package/dist/render/util/pcolumn_data.cjs.map +1 -1
  104. package/dist/render/util/pcolumn_data.d.ts +1 -1
  105. package/dist/render/util/pcolumn_data.d.ts.map +1 -1
  106. package/dist/render/util/pcolumn_data.js.map +1 -1
  107. package/package.json +8 -6
  108. package/src/annotations/converter.ts +12 -40
  109. package/src/block_model.ts +1 -0
  110. package/src/builder.ts +1 -0
  111. package/src/components/PlDataTable/index.ts +22 -0
  112. package/src/components/PlDataTable/labels.ts +101 -0
  113. package/src/components/PlDataTable/state-migration.ts +285 -0
  114. package/src/components/PlDataTable/table.ts +279 -0
  115. package/src/components/PlDataTable/v4.ts +193 -0
  116. package/src/components/PlDataTable/v5.ts +140 -0
  117. package/src/filters/converters/filterEmpty.test.ts +125 -0
  118. package/src/filters/converters/filterEmpty.ts +57 -0
  119. package/src/filters/converters/filterToQuery.test.ts +417 -0
  120. package/src/filters/converters/filterToQuery.ts +258 -0
  121. package/src/filters/{converter.test.ts → converters/filterUiToExpressionImpl.test.ts} +2 -2
  122. package/src/filters/{converter.ts → converters/filterUiToExpressionImpl.ts} +3 -2
  123. package/src/filters/converters/index.ts +2 -0
  124. package/src/filters/distill.ts +59 -0
  125. package/src/filters/index.ts +2 -1
  126. package/src/filters/types.ts +8 -48
  127. package/src/pframe_utils/querySpec.ts +1 -0
  128. package/src/render/api.ts +13 -6
  129. package/src/render/index.ts +2 -1
  130. package/src/render/internal.ts +11 -0
  131. package/src/render/util/column_collection.ts +1 -1
  132. package/src/render/util/pcolumn_data.ts +1 -1
  133. package/dist/components/PlDataTable.cjs +0 -307
  134. package/dist/components/PlDataTable.cjs.map +0 -1
  135. package/dist/components/PlDataTable.d.ts +0 -366
  136. package/dist/components/PlDataTable.d.ts.map +0 -1
  137. package/dist/components/PlDataTable.js +0 -297
  138. package/dist/components/PlDataTable.js.map +0 -1
  139. package/dist/filters/converter.cjs.map +0 -1
  140. package/dist/filters/converter.d.ts.map +0 -1
  141. package/dist/filters/converter.js.map +0 -1
  142. package/src/components/PlDataTable.ts +0 -794
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { FilterSpec, FilterSpecLeaf } from "@milaboratories/pl-model-common";
3
+ import { filterEmptyPieces, filterPredicate } from "./filterEmpty";
4
+
5
+ type F = FilterSpec<FilterSpecLeaf<string>>;
6
+
7
+ const leaf: F = { type: "equal", column: "c1", x: 1 };
8
+ const emptyLeaf: F = { type: undefined };
9
+ const incompleteLeaf: F = { type: "equal", column: "c1", x: undefined as unknown as number };
10
+
11
+ describe("filterPredicate", () => {
12
+ it("returns false for undefined type", () => {
13
+ expect(filterPredicate(emptyLeaf)).toBe(false);
14
+ });
15
+
16
+ it("returns true for a valid leaf", () => {
17
+ expect(filterPredicate(leaf)).toBe(true);
18
+ });
19
+
20
+ it("returns false for a leaf with undefined values", () => {
21
+ expect(filterPredicate(incompleteLeaf)).toBe(false);
22
+ });
23
+
24
+ it("returns false for and with empty filters", () => {
25
+ expect(filterPredicate({ type: "and", filters: [] })).toBe(false);
26
+ });
27
+
28
+ it("returns true for and with filters", () => {
29
+ expect(filterPredicate({ type: "and", filters: [leaf] })).toBe(true);
30
+ });
31
+
32
+ it("returns false for or with empty filters", () => {
33
+ expect(filterPredicate({ type: "or", filters: [] })).toBe(false);
34
+ });
35
+
36
+ it("returns false for not wrapping an empty filter", () => {
37
+ expect(filterPredicate({ type: "not", filter: emptyLeaf })).toBe(false);
38
+ });
39
+
40
+ it("returns true for not wrapping a valid filter", () => {
41
+ expect(filterPredicate({ type: "not", filter: leaf })).toBe(true);
42
+ });
43
+ });
44
+
45
+ describe("filterEmptyPieces", () => {
46
+ it("returns valid leaf as-is", () => {
47
+ expect(filterEmptyPieces(leaf)).toEqual(leaf);
48
+ });
49
+
50
+ it("returns null for empty leaf", () => {
51
+ expect(filterEmptyPieces(emptyLeaf)).toBeNull();
52
+ });
53
+
54
+ it("returns null for incomplete leaf", () => {
55
+ expect(filterEmptyPieces(incompleteLeaf)).toBeNull();
56
+ });
57
+
58
+ // --- and ---
59
+
60
+ it("filters out empty children from and", () => {
61
+ const filter: F = { type: "and", filters: [emptyLeaf, leaf, emptyLeaf] };
62
+ expect(filterEmptyPieces(filter)).toEqual({ type: "and", filters: [leaf] });
63
+ });
64
+
65
+ it("returns null when all and children are empty", () => {
66
+ const filter: F = { type: "and", filters: [emptyLeaf, emptyLeaf] };
67
+ expect(filterEmptyPieces(filter)).toBeNull();
68
+ });
69
+
70
+ it("returns null for and with no children", () => {
71
+ const filter: F = { type: "and", filters: [] };
72
+ expect(filterEmptyPieces(filter)).toBeNull();
73
+ });
74
+
75
+ // --- or ---
76
+
77
+ it("filters out empty children from or", () => {
78
+ const leaf2: F = { type: "equal", column: "c2", x: 2 };
79
+ const filter: F = { type: "or", filters: [leaf, emptyLeaf, leaf2] };
80
+ expect(filterEmptyPieces(filter)).toEqual({ type: "or", filters: [leaf, leaf2] });
81
+ });
82
+
83
+ it("returns null when all or children are empty", () => {
84
+ const filter: F = { type: "or", filters: [emptyLeaf] };
85
+ expect(filterEmptyPieces(filter)).toBeNull();
86
+ });
87
+
88
+ // --- not ---
89
+
90
+ it("preserves not with valid inner filter", () => {
91
+ const filter: F = { type: "not", filter: leaf };
92
+ expect(filterEmptyPieces(filter)).toEqual({ type: "not", filter: leaf });
93
+ });
94
+
95
+ it("returns null for not wrapping an empty filter", () => {
96
+ const filter: F = { type: "not", filter: emptyLeaf };
97
+ expect(filterEmptyPieces(filter)).toBeNull();
98
+ });
99
+
100
+ // --- nested collapse ---
101
+
102
+ it("returns null when nested and collapses", () => {
103
+ const filter: F = {
104
+ type: "not",
105
+ filter: { type: "and", filters: [emptyLeaf] },
106
+ };
107
+ expect(filterEmptyPieces(filter)).toBeNull();
108
+ });
109
+
110
+ it("returns null when parent and has only a collapsing child", () => {
111
+ const filter: F = {
112
+ type: "and",
113
+ filters: [{ type: "or", filters: [emptyLeaf] }],
114
+ };
115
+ expect(filterEmptyPieces(filter)).toBeNull();
116
+ });
117
+
118
+ it("keeps valid siblings when one child collapses", () => {
119
+ const filter: F = {
120
+ type: "and",
121
+ filters: [{ type: "or", filters: [emptyLeaf] }, leaf],
122
+ };
123
+ expect(filterEmptyPieces(filter)).toEqual({ type: "and", filters: [leaf] });
124
+ });
125
+ });
@@ -0,0 +1,57 @@
1
+ import { FilterSpec, FilterSpecLeaf } from "@milaboratories/pl-model-common";
2
+
3
+ export function filterPredicate(
4
+ item: FilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>,
5
+ ): boolean {
6
+ // No need to convert empty steps
7
+ if (item.type == null) {
8
+ return false;
9
+ }
10
+
11
+ if (item.type === "or") {
12
+ return item.filters.length > 0;
13
+ }
14
+
15
+ if (item.type === "and") {
16
+ return item.filters.length > 0;
17
+ }
18
+
19
+ if (item.type === "not") {
20
+ return filterPredicate(item.filter);
21
+ }
22
+
23
+ // Filter out any item that has undefined values in required fields
24
+ return !Object.values(item).some((v) => v === undefined);
25
+ }
26
+
27
+ export function filterEmptyPieces<T extends FilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>>(
28
+ item: T,
29
+ ): null | T {
30
+ if (item.type === "or" || item.type === "and") {
31
+ const filtered = item.filters
32
+ .map(filterEmptyPieces)
33
+ .filter((f): f is T => f !== null && filterPredicate(f));
34
+
35
+ return filtered.length === 0
36
+ ? null
37
+ : {
38
+ ...item,
39
+ filters: filtered,
40
+ };
41
+ }
42
+ if (item.type === "not") {
43
+ const inner = filterEmptyPieces(item.filter);
44
+ return inner === null || !filterPredicate(inner)
45
+ ? null
46
+ : {
47
+ ...item,
48
+ filter: inner,
49
+ };
50
+ }
51
+
52
+ if (!filterPredicate(item)) {
53
+ return null;
54
+ }
55
+
56
+ return item;
57
+ }
@@ -0,0 +1,417 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { FilterSpec, FilterSpecLeaf } from "@milaboratories/pl-model-common";
3
+ import { filterSpecToSpecQueryExpr } from "./filterToQuery";
4
+
5
+ type QFilterSpec = FilterSpec<FilterSpecLeaf<string>>;
6
+
7
+ /** Helper: creates a CanonicalizedJson<PTableColumnId> for a regular column. */
8
+ function colRef(id: string): string {
9
+ return JSON.stringify({ type: "column", id });
10
+ }
11
+
12
+ /** Helper: creates a CanonicalizedJson<PTableColumnId> for an axis. */
13
+ function axisRef(id: string): string {
14
+ return JSON.stringify({ type: "axis", id });
15
+ }
16
+
17
+ describe("filterSpecToSpecQueryExpr", () => {
18
+ // --- logical combinators ---
19
+
20
+ it('should convert "and" filter', () => {
21
+ const filter: QFilterSpec = {
22
+ type: "and",
23
+ filters: [
24
+ { type: "equal", column: colRef("c1"), x: 1 },
25
+ { type: "equal", column: colRef("c2"), x: 2 },
26
+ ],
27
+ };
28
+ const result = filterSpecToSpecQueryExpr(filter);
29
+ expect(result.type).toBe("and");
30
+ if (result.type === "and") {
31
+ expect(result.input).toHaveLength(2);
32
+ }
33
+ });
34
+
35
+ it('should convert "or" filter', () => {
36
+ const filter: QFilterSpec = {
37
+ type: "or",
38
+ filters: [
39
+ { type: "equal", column: colRef("c1"), x: 1 },
40
+ { type: "equal", column: colRef("c2"), x: 2 },
41
+ ],
42
+ };
43
+ const result = filterSpecToSpecQueryExpr(filter);
44
+ expect(result.type).toBe("or");
45
+ if (result.type === "or") {
46
+ expect(result.input).toHaveLength(2);
47
+ }
48
+ });
49
+
50
+ it('should convert "not" filter', () => {
51
+ const filter: QFilterSpec = {
52
+ type: "not",
53
+ filter: { type: "equal", column: colRef("c1"), x: 5 },
54
+ };
55
+ const result = filterSpecToSpecQueryExpr(filter);
56
+ expect(result.type).toBe("not");
57
+ if (result.type === "not") {
58
+ expect(result.input.type).toBe("numericComparison");
59
+ }
60
+ });
61
+
62
+ it("should skip filters with undefined type in and/or", () => {
63
+ const filter: QFilterSpec = {
64
+ type: "and",
65
+ filters: [{ type: undefined }, { type: "equal", column: colRef("c1"), x: 1 }],
66
+ };
67
+ const result = filterSpecToSpecQueryExpr(filter);
68
+ expect(result.type).toBe("and");
69
+ if (result.type === "and") {
70
+ expect(result.input).toHaveLength(1);
71
+ expect(result.input[0].type).toBe("numericComparison");
72
+ }
73
+ });
74
+
75
+ it("should throw for empty and filter (after skipping undefined)", () => {
76
+ const filter: QFilterSpec = {
77
+ type: "and",
78
+ filters: [{ type: undefined }],
79
+ };
80
+ expect(() => filterSpecToSpecQueryExpr(filter)).toThrow(
81
+ "AND filter requires at least one operand",
82
+ );
83
+ });
84
+
85
+ it("should throw for empty or filter", () => {
86
+ const filter: QFilterSpec = { type: "or", filters: [] };
87
+ expect(() => filterSpecToSpecQueryExpr(filter)).toThrow(
88
+ "OR filter requires at least one operand",
89
+ );
90
+ });
91
+
92
+ // --- string filters ---
93
+
94
+ it('should convert "patternEquals" filter', () => {
95
+ const filter: QFilterSpec = {
96
+ type: "patternEquals",
97
+ column: colRef("col1"),
98
+ value: "abc",
99
+ };
100
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
101
+ type: "stringEquals",
102
+ input: { type: "columnRef", value: "col1" },
103
+ value: "abc",
104
+ caseInsensitive: false,
105
+ });
106
+ });
107
+
108
+ it('should convert "patternNotEquals" filter', () => {
109
+ const filter: QFilterSpec = {
110
+ type: "patternNotEquals",
111
+ column: colRef("col1"),
112
+ value: "abc",
113
+ };
114
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
115
+ type: "not",
116
+ input: {
117
+ type: "stringEquals",
118
+ input: { type: "columnRef", value: "col1" },
119
+ value: "abc",
120
+ caseInsensitive: false,
121
+ },
122
+ });
123
+ });
124
+
125
+ it('should convert "patternContainSubsequence" filter', () => {
126
+ const filter: QFilterSpec = {
127
+ type: "patternContainSubsequence",
128
+ column: colRef("col1"),
129
+ value: "sub",
130
+ };
131
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
132
+ type: "stringContains",
133
+ input: { type: "columnRef", value: "col1" },
134
+ value: "sub",
135
+ caseInsensitive: false,
136
+ });
137
+ });
138
+
139
+ it('should convert "patternNotContainSubsequence" filter', () => {
140
+ const filter: QFilterSpec = {
141
+ type: "patternNotContainSubsequence",
142
+ column: colRef("col1"),
143
+ value: "sub",
144
+ };
145
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
146
+ type: "not",
147
+ input: {
148
+ type: "stringContains",
149
+ input: { type: "columnRef", value: "col1" },
150
+ value: "sub",
151
+ caseInsensitive: false,
152
+ },
153
+ });
154
+ });
155
+
156
+ it('should convert "patternMatchesRegularExpression" filter', () => {
157
+ const filter: QFilterSpec = {
158
+ type: "patternMatchesRegularExpression",
159
+ column: colRef("col1"),
160
+ value: "^abc.*",
161
+ };
162
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
163
+ type: "stringRegex",
164
+ input: { type: "columnRef", value: "col1" },
165
+ value: "^abc.*",
166
+ });
167
+ });
168
+
169
+ it('should convert "patternFuzzyContainSubsequence" filter with defaults', () => {
170
+ const filter: QFilterSpec = {
171
+ type: "patternFuzzyContainSubsequence",
172
+ column: colRef("col1"),
173
+ value: "fuz",
174
+ };
175
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
176
+ type: "stringContainsFuzzy",
177
+ input: { type: "columnRef", value: "col1" },
178
+ value: "fuz",
179
+ maxEdits: 1,
180
+ caseInsensitive: false,
181
+ substitutionsOnly: false,
182
+ wildcard: null,
183
+ });
184
+ });
185
+
186
+ it('should convert "patternFuzzyContainSubsequence" filter with custom options', () => {
187
+ const filter: QFilterSpec = {
188
+ type: "patternFuzzyContainSubsequence",
189
+ column: colRef("col1"),
190
+ value: "fuz",
191
+ maxEdits: 3,
192
+ substitutionsOnly: true,
193
+ wildcard: "?",
194
+ };
195
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
196
+ type: "stringContainsFuzzy",
197
+ input: { type: "columnRef", value: "col1" },
198
+ value: "fuz",
199
+ maxEdits: 3,
200
+ caseInsensitive: false,
201
+ substitutionsOnly: true,
202
+ wildcard: "?",
203
+ });
204
+ });
205
+
206
+ // --- numeric comparison filters ---
207
+
208
+ it("should convert numeric comparison filters", () => {
209
+ const testCases = [
210
+ { type: "equal" as const, operand: "eq" },
211
+ { type: "notEqual" as const, operand: "ne" },
212
+ { type: "lessThan" as const, operand: "lt" },
213
+ { type: "greaterThan" as const, operand: "gt" },
214
+ { type: "lessThanOrEqual" as const, operand: "le" },
215
+ { type: "greaterThanOrEqual" as const, operand: "ge" },
216
+ ];
217
+
218
+ testCases.forEach(({ type, operand }) => {
219
+ const filter: QFilterSpec = { type, column: colRef("num"), x: 42 };
220
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
221
+ type: "numericComparison",
222
+ operand,
223
+ left: { type: "columnRef", value: "num" },
224
+ right: { type: "constant", value: 42 },
225
+ });
226
+ });
227
+ });
228
+
229
+ // --- column-to-column comparisons ---
230
+
231
+ it('should convert "equalToColumn" filter', () => {
232
+ const filter: QFilterSpec = {
233
+ type: "equalToColumn",
234
+ column: colRef("c1"),
235
+ rhs: colRef("c2"),
236
+ };
237
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
238
+ type: "numericComparison",
239
+ operand: "eq",
240
+ left: { type: "columnRef", value: "c1" },
241
+ right: { type: "columnRef", value: "c2" },
242
+ });
243
+ });
244
+
245
+ it("should convert column comparison filters without minDiff", () => {
246
+ const cases = [
247
+ { type: "lessThanColumn" as const, operand: "lt" },
248
+ { type: "greaterThanColumn" as const, operand: "gt" },
249
+ { type: "lessThanColumnOrEqual" as const, operand: "le" },
250
+ { type: "greaterThanColumnOrEqual" as const, operand: "ge" },
251
+ ];
252
+
253
+ cases.forEach(({ type, operand }) => {
254
+ const filter: QFilterSpec = { type, column: colRef("c1"), rhs: colRef("c2") };
255
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
256
+ type: "numericComparison",
257
+ operand,
258
+ left: { type: "columnRef", value: "c1" },
259
+ right: { type: "columnRef", value: "c2" },
260
+ });
261
+ });
262
+ });
263
+
264
+ it("should convert column comparison filters with minDiff", () => {
265
+ const cases = [
266
+ { type: "lessThanColumn" as const, operand: "lt" },
267
+ { type: "greaterThanColumn" as const, operand: "gt" },
268
+ { type: "lessThanColumnOrEqual" as const, operand: "le" },
269
+ { type: "greaterThanColumnOrEqual" as const, operand: "ge" },
270
+ ];
271
+
272
+ cases.forEach(({ type, operand }) => {
273
+ const filter: QFilterSpec = { type, column: colRef("c1"), rhs: colRef("c2"), minDiff: 5 };
274
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
275
+ type: "numericComparison",
276
+ operand,
277
+ left: {
278
+ type: "numericBinary",
279
+ operand: "add",
280
+ left: { type: "columnRef", value: "c1" },
281
+ right: { type: "constant", value: 5 },
282
+ },
283
+ right: { type: "columnRef", value: "c2" },
284
+ });
285
+ });
286
+ });
287
+
288
+ it("should treat minDiff=0 as no minDiff", () => {
289
+ const filter: QFilterSpec = {
290
+ type: "lessThanColumn",
291
+ column: colRef("c1"),
292
+ rhs: colRef("c2"),
293
+ minDiff: 0,
294
+ };
295
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
296
+ type: "numericComparison",
297
+ operand: "lt",
298
+ left: { type: "columnRef", value: "c1" },
299
+ right: { type: "columnRef", value: "c2" },
300
+ });
301
+ });
302
+
303
+ // --- set filters ---
304
+
305
+ it('should convert "inSet" filter', () => {
306
+ const filter: QFilterSpec = {
307
+ type: "inSet",
308
+ column: colRef("col1"),
309
+ value: ["a", "b", "c"],
310
+ };
311
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
312
+ type: "isIn",
313
+ input: { type: "columnRef", value: "col1" },
314
+ set: ["a", "b", "c"],
315
+ });
316
+ });
317
+
318
+ it('should convert "notInSet" filter', () => {
319
+ const filter: QFilterSpec = {
320
+ type: "notInSet",
321
+ column: colRef("col1"),
322
+ value: ["x"],
323
+ };
324
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
325
+ type: "not",
326
+ input: {
327
+ type: "isIn",
328
+ input: { type: "columnRef", value: "col1" },
329
+ set: ["x"],
330
+ },
331
+ });
332
+ });
333
+
334
+ // --- axis references ---
335
+
336
+ it("should resolve axis column references", () => {
337
+ const filter: QFilterSpec = {
338
+ type: "equal",
339
+ column: axisRef("pl7.app/sampleId"),
340
+ x: 10,
341
+ };
342
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
343
+ type: "numericComparison",
344
+ operand: "eq",
345
+ left: { type: "axisRef", value: "pl7.app/sampleId" },
346
+ right: { type: "constant", value: 10 },
347
+ });
348
+ });
349
+
350
+ // --- null check filters ---
351
+
352
+ it('should convert "isNA" filter', () => {
353
+ const filter: QFilterSpec = { type: "isNA", column: colRef("c1") };
354
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
355
+ type: "isNull",
356
+ input: { type: "columnRef", value: "c1" },
357
+ });
358
+ });
359
+
360
+ it('should convert "isNotNA" filter', () => {
361
+ const filter: QFilterSpec = { type: "isNotNA", column: colRef("c1") };
362
+ expect(filterSpecToSpecQueryExpr(filter)).toEqual({
363
+ type: "not",
364
+ input: {
365
+ type: "isNull",
366
+ input: { type: "columnRef", value: "c1" },
367
+ },
368
+ });
369
+ });
370
+
371
+ // --- unsupported types ---
372
+
373
+ it("should throw for topN filter", () => {
374
+ const filter: QFilterSpec = { type: "topN", column: colRef("c1"), n: 5 };
375
+ expect(() => filterSpecToSpecQueryExpr(filter)).toThrow('Filter type "topN" is not supported');
376
+ });
377
+
378
+ it("should throw for bottomN filter", () => {
379
+ const filter: QFilterSpec = { type: "bottomN", column: colRef("c1"), n: 3 };
380
+ expect(() => filterSpecToSpecQueryExpr(filter)).toThrow(
381
+ 'Filter type "bottomN" is not supported',
382
+ );
383
+ });
384
+
385
+ it("should throw for undefined filter type", () => {
386
+ const filter: QFilterSpec = { type: undefined };
387
+ expect(() => filterSpecToSpecQueryExpr(filter)).toThrow("Filter type is undefined");
388
+ });
389
+
390
+ // --- nested ---
391
+
392
+ it("should convert nested and/or/not filters", () => {
393
+ const filter: QFilterSpec = {
394
+ type: "and",
395
+ filters: [
396
+ {
397
+ type: "or",
398
+ filters: [
399
+ { type: "patternEquals", column: colRef("col1"), value: "a" },
400
+ { type: "patternEquals", column: colRef("col1"), value: "b" },
401
+ ],
402
+ },
403
+ {
404
+ type: "not",
405
+ filter: { type: "greaterThan", column: colRef("num"), x: 100 },
406
+ },
407
+ ],
408
+ };
409
+ const result = filterSpecToSpecQueryExpr(filter);
410
+ expect(result.type).toBe("and");
411
+ if (result.type === "and") {
412
+ expect(result.input).toHaveLength(2);
413
+ expect(result.input[0].type).toBe("or");
414
+ expect(result.input[1].type).toBe("not");
415
+ }
416
+ });
417
+ });