@platforma-sdk/model 1.63.12 → 1.64.0

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 (162) hide show
  1. package/dist/columns/column_collection_builder.cjs +105 -92
  2. package/dist/columns/column_collection_builder.cjs.map +1 -1
  3. package/dist/columns/column_collection_builder.d.ts +13 -12
  4. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  5. package/dist/columns/column_collection_builder.js +107 -94
  6. package/dist/columns/column_collection_builder.js.map +1 -1
  7. package/dist/columns/column_selector.cjs +8 -80
  8. package/dist/columns/column_selector.cjs.map +1 -1
  9. package/dist/columns/column_selector.d.ts +6 -14
  10. package/dist/columns/column_selector.d.ts.map +1 -1
  11. package/dist/columns/column_selector.js +6 -77
  12. package/dist/columns/column_selector.js.map +1 -1
  13. package/dist/columns/column_snapshot.cjs +3 -3
  14. package/dist/columns/column_snapshot.cjs.map +1 -1
  15. package/dist/columns/column_snapshot.d.ts +3 -3
  16. package/dist/columns/column_snapshot.d.ts.map +1 -1
  17. package/dist/columns/column_snapshot.js +3 -3
  18. package/dist/columns/column_snapshot.js.map +1 -1
  19. package/dist/columns/column_snapshot_provider.cjs +1 -1
  20. package/dist/columns/column_snapshot_provider.cjs.map +1 -1
  21. package/dist/columns/column_snapshot_provider.d.ts +8 -8
  22. package/dist/columns/column_snapshot_provider.d.ts.map +1 -1
  23. package/dist/columns/column_snapshot_provider.js +1 -1
  24. package/dist/columns/column_snapshot_provider.js.map +1 -1
  25. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  26. package/dist/columns/ctx_column_sources.d.ts +2 -1
  27. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  28. package/dist/columns/ctx_column_sources.js.map +1 -1
  29. package/dist/columns/expand_by_partition.cjs +106 -0
  30. package/dist/columns/expand_by_partition.cjs.map +1 -0
  31. package/dist/columns/expand_by_partition.d.ts +33 -0
  32. package/dist/columns/expand_by_partition.d.ts.map +1 -0
  33. package/dist/columns/expand_by_partition.js +105 -0
  34. package/dist/columns/expand_by_partition.js.map +1 -0
  35. package/dist/columns/index.cjs +1 -0
  36. package/dist/columns/index.d.ts +4 -3
  37. package/dist/columns/index.js +1 -0
  38. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +26 -0
  39. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -0
  40. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +25 -0
  41. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -0
  42. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs +68 -0
  43. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs.map +1 -0
  44. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js +67 -0
  45. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js.map +1 -0
  46. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs +20 -9
  47. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs.map +1 -1
  48. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts +4 -0
  49. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts.map +1 -1
  50. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js +21 -10
  51. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js.map +1 -1
  52. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +257 -175
  53. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  54. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +37 -21
  55. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  56. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +260 -175
  57. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  58. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +64 -0
  59. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -0
  60. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +17 -0
  61. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -0
  62. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +63 -0
  63. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -0
  64. package/dist/components/PlDataTable/createPlDataTable/index.cjs +2 -1
  65. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -1
  66. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +2 -1
  67. package/dist/components/PlDataTable/createPlDataTable/index.d.ts.map +1 -1
  68. package/dist/components/PlDataTable/createPlDataTable/index.js +2 -1
  69. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -1
  70. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +109 -0
  71. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -0
  72. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts +19 -0
  73. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts.map +1 -0
  74. package/dist/components/PlDataTable/createPlDataTable/utils.js +102 -0
  75. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -0
  76. package/dist/components/PlDataTable/index.cjs +3 -1
  77. package/dist/components/PlDataTable/index.d.ts +4 -2
  78. package/dist/components/PlDataTable/index.js +3 -1
  79. package/dist/components/PlDataTable/labels.cjs +25 -11
  80. package/dist/components/PlDataTable/labels.cjs.map +1 -1
  81. package/dist/components/PlDataTable/labels.js +25 -11
  82. package/dist/components/PlDataTable/labels.js.map +1 -1
  83. package/dist/components/PlDataTable/state-migration.cjs +4 -1
  84. package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
  85. package/dist/components/PlDataTable/state-migration.d.ts.map +1 -1
  86. package/dist/components/PlDataTable/state-migration.js +4 -1
  87. package/dist/components/PlDataTable/state-migration.js.map +1 -1
  88. package/dist/components/PlDataTable/typesV5.d.ts +5 -4
  89. package/dist/components/PlDataTable/typesV5.d.ts.map +1 -1
  90. package/dist/components/index.cjs +3 -1
  91. package/dist/components/index.d.ts +3 -1
  92. package/dist/components/index.js +3 -1
  93. package/dist/index.cjs +13 -9
  94. package/dist/index.d.ts +8 -6
  95. package/dist/index.js +6 -4
  96. package/dist/labels/derive_distinct_labels.cjs +39 -27
  97. package/dist/labels/derive_distinct_labels.cjs.map +1 -1
  98. package/dist/labels/derive_distinct_labels.d.ts +15 -15
  99. package/dist/labels/derive_distinct_labels.d.ts.map +1 -1
  100. package/dist/labels/derive_distinct_labels.js +39 -27
  101. package/dist/labels/derive_distinct_labels.js.map +1 -1
  102. package/dist/labels/index.cjs +0 -1
  103. package/dist/labels/index.d.ts +1 -2
  104. package/dist/labels/index.js +0 -1
  105. package/dist/package.cjs +1 -1
  106. package/dist/package.js +1 -1
  107. package/dist/render/api.cjs +10 -3
  108. package/dist/render/api.cjs.map +1 -1
  109. package/dist/render/api.d.ts +2 -2
  110. package/dist/render/api.d.ts.map +1 -1
  111. package/dist/render/api.js +10 -3
  112. package/dist/render/api.js.map +1 -1
  113. package/dist/render/util/column_collection.cjs +3 -3
  114. package/dist/render/util/column_collection.cjs.map +1 -1
  115. package/dist/render/util/column_collection.d.ts.map +1 -1
  116. package/dist/render/util/column_collection.js +3 -3
  117. package/dist/render/util/column_collection.js.map +1 -1
  118. package/dist/render/util/label.cjs +2 -2
  119. package/dist/render/util/label.cjs.map +1 -1
  120. package/dist/render/util/label.js +2 -2
  121. package/dist/render/util/label.js.map +1 -1
  122. package/dist/render/util/pcolumn_data.cjs.map +1 -1
  123. package/dist/render/util/pcolumn_data.d.ts +2 -2
  124. package/dist/render/util/pcolumn_data.d.ts.map +1 -1
  125. package/dist/render/util/pcolumn_data.js.map +1 -1
  126. package/package.json +6 -6
  127. package/src/columns/column_collection_builder.test.ts +40 -27
  128. package/src/columns/column_collection_builder.ts +176 -131
  129. package/src/columns/column_selector.test.ts +17 -399
  130. package/src/columns/column_selector.ts +14 -127
  131. package/src/columns/column_snapshot.ts +5 -5
  132. package/src/columns/column_snapshot_provider.ts +11 -10
  133. package/src/columns/ctx_column_sources.ts +2 -2
  134. package/src/columns/expand_by_partition.test.ts +4 -4
  135. package/src/columns/expand_by_partition.ts +4 -3
  136. package/src/columns/index.ts +1 -0
  137. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +42 -0
  138. package/src/components/PlDataTable/createPlDataTable/createPTableDefV3.ts +89 -0
  139. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV2.ts +39 -11
  140. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +502 -313
  141. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +122 -0
  142. package/src/components/PlDataTable/createPlDataTable/index.ts +4 -2
  143. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +257 -0
  144. package/src/components/PlDataTable/createPlDataTable/utils.ts +160 -0
  145. package/src/components/PlDataTable/index.ts +13 -2
  146. package/src/components/PlDataTable/labels.ts +29 -18
  147. package/src/components/PlDataTable/state-migration.ts +3 -1
  148. package/src/components/PlDataTable/typesV5.ts +4 -4
  149. package/src/labels/derive_distinct_labels.test.ts +143 -45
  150. package/src/labels/derive_distinct_labels.ts +102 -49
  151. package/src/labels/index.ts +0 -1
  152. package/src/render/api.ts +15 -5
  153. package/src/render/util/column_collection.ts +4 -3
  154. package/src/render/util/label.ts +2 -2
  155. package/src/render/util/pcolumn_data.ts +5 -3
  156. package/dist/labels/write_labels_to_specs.cjs +0 -14
  157. package/dist/labels/write_labels_to_specs.cjs.map +0 -1
  158. package/dist/labels/write_labels_to_specs.d.ts +0 -7
  159. package/dist/labels/write_labels_to_specs.d.ts.map +0 -1
  160. package/dist/labels/write_labels_to_specs.js +0 -13
  161. package/dist/labels/write_labels_to_specs.js.map +0 -1
  162. package/src/labels/write_labels_to_specs.ts +0 -12
@@ -1,47 +1,28 @@
1
- import type { MultiColumnSelector, PColumnSpec } from "@milaboratories/pl-model-common";
2
1
  import { describe, expect, test } from "vitest";
3
- import type { RelaxedColumnSelector } from "./column_selector";
4
- import {
5
- matchColumn,
6
- matchColumnSelectors,
7
- normalizeSelectors,
8
- columnSelectorsToPredicate,
9
- } from "./column_selector";
2
+ import { convertColumnSelectorToMultiColumnSelector } from "./column_selector";
10
3
  import type { RegExpString } from "@milaboratories/helpers";
11
4
 
12
- // --- Helpers ---
5
+ // --- convertColumnSelectorToMultiColumnSelector ---
13
6
 
14
- function spec(overrides: Partial<PColumnSpec> & { name: string }): PColumnSpec {
15
- return {
16
- kind: "PColumn",
17
- valueType: "Int",
18
- axesSpec: [],
19
- annotations: {},
20
- ...overrides,
21
- } as PColumnSpec;
22
- }
23
-
24
- // --- normalizeSelectors ---
25
-
26
- describe("normalizeSelectors", () => {
7
+ describe("convertColumnSelectorToMultiColumnSelector", () => {
27
8
  test("wraps single selector in array", () => {
28
- const result = normalizeSelectors({ name: "foo" });
9
+ const result = convertColumnSelectorToMultiColumnSelector({ name: "foo" });
29
10
  expect(result).toHaveLength(1);
30
11
  expect(result[0].name).toEqual([{ type: "regex", value: "foo" }]);
31
12
  });
32
13
 
33
14
  test("passes through array of selectors", () => {
34
- const result = normalizeSelectors([{ name: "a" }, { name: "b" }]);
15
+ const result = convertColumnSelectorToMultiColumnSelector([{ name: "a" }, { name: "b" }]);
35
16
  expect(result).toHaveLength(2);
36
17
  });
37
18
 
38
19
  test("normalizes plain string name to RegexMatcher[]", () => {
39
- const [sel] = normalizeSelectors({ name: "foo" });
20
+ const [sel] = convertColumnSelectorToMultiColumnSelector({ name: "foo" });
40
21
  expect(sel.name).toEqual([{ type: "regex", value: "foo" }]);
41
22
  });
42
23
 
43
24
  test("normalizes array of mixed strings and matchers", () => {
44
- const [sel] = normalizeSelectors({
25
+ const [sel] = convertColumnSelectorToMultiColumnSelector({
45
26
  name: ["foo", { type: "regex", value: "bar.*" as RegExpString }],
46
27
  });
47
28
  expect(sel.name).toEqual([
@@ -51,22 +32,24 @@ describe("normalizeSelectors", () => {
51
32
  });
52
33
 
53
34
  test("normalizes single StringMatcher object", () => {
54
- const [sel] = normalizeSelectors({ name: { type: "exact", value: "foo" } });
35
+ const [sel] = convertColumnSelectorToMultiColumnSelector({
36
+ name: { type: "exact", value: "foo" },
37
+ });
55
38
  expect(sel.name).toEqual([{ type: "exact", value: "foo" }]);
56
39
  });
57
40
 
58
41
  test("normalizes single ValueType to array", () => {
59
- const [sel] = normalizeSelectors({ type: "Int" });
42
+ const [sel] = convertColumnSelectorToMultiColumnSelector({ type: "Int" });
60
43
  expect(sel.type).toEqual(["Int"]);
61
44
  });
62
45
 
63
46
  test("passes through ValueType array", () => {
64
- const [sel] = normalizeSelectors({ type: ["Int", "Long"] });
47
+ const [sel] = convertColumnSelectorToMultiColumnSelector({ type: ["Int", "Long"] });
65
48
  expect(sel.type).toEqual(["Int", "Long"]);
66
49
  });
67
50
 
68
51
  test("normalizes record with plain string values", () => {
69
- const [sel] = normalizeSelectors({
52
+ const [sel] = convertColumnSelectorToMultiColumnSelector({
70
53
  domain: { "pl7.app/chain": "IGHeavy" },
71
54
  });
72
55
  expect(sel.domain).toEqual({
@@ -75,7 +58,7 @@ describe("normalizeSelectors", () => {
75
58
  });
76
59
 
77
60
  test("normalizes record with mixed array values", () => {
78
- const [sel] = normalizeSelectors({
61
+ const [sel] = convertColumnSelectorToMultiColumnSelector({
79
62
  annotations: { label: ["a", { type: "regex", value: "b.*" as RegExpString }] },
80
63
  });
81
64
  expect(sel.annotations).toEqual({
@@ -87,386 +70,21 @@ describe("normalizeSelectors", () => {
87
70
  });
88
71
 
89
72
  test("normalizes axes", () => {
90
- const [sel] = normalizeSelectors({
73
+ const [sel] = convertColumnSelectorToMultiColumnSelector({
91
74
  axes: [{ name: "axisName", type: "String" }],
92
75
  });
93
76
  expect(sel.axes).toEqual([{ name: [{ type: "regex", value: "axisName" }], type: ["String"] }]);
94
77
  });
95
78
 
96
79
  test("preserves partialAxesMatch", () => {
97
- const [sel] = normalizeSelectors({ partialAxesMatch: false });
80
+ const [sel] = convertColumnSelectorToMultiColumnSelector({ partialAxesMatch: false });
98
81
  expect(sel.partialAxesMatch).toBe(false);
99
82
  });
100
83
 
101
84
  test("omits undefined fields", () => {
102
- const [sel] = normalizeSelectors({ name: "foo" });
85
+ const [sel] = convertColumnSelectorToMultiColumnSelector({ name: "foo" });
103
86
  expect(sel.type).toBeUndefined();
104
87
  expect(sel.domain).toBeUndefined();
105
88
  expect(sel.axes).toBeUndefined();
106
89
  });
107
90
  });
108
-
109
- // --- matchColumn ---
110
-
111
- describe("matchColumn", () => {
112
- describe("name matching", () => {
113
- test("exact name match", () => {
114
- const s = spec({ name: "pl7.app/vdj/sequence" });
115
- const sel: MultiColumnSelector = { name: [{ type: "exact", value: "pl7.app/vdj/sequence" }] };
116
- expect(matchColumn(s, sel)).toBe(true);
117
- });
118
-
119
- test("exact name mismatch", () => {
120
- const s = spec({ name: "pl7.app/vdj/sequence" });
121
- const sel: MultiColumnSelector = { name: [{ type: "exact", value: "pl7.app/vdj/other" }] };
122
- expect(matchColumn(s, sel)).toBe(false);
123
- });
124
-
125
- test("regex name match", () => {
126
- const s = spec({ name: "pl7.app/vdj/sequence" });
127
- const sel: MultiColumnSelector = {
128
- name: [{ type: "regex", value: "pl7\\.app/vdj/.*" as RegExpString }],
129
- };
130
- expect(matchColumn(s, sel)).toBe(true);
131
- });
132
-
133
- test("regex full match required", () => {
134
- const s = spec({ name: "pl7.app/vdj/sequence" });
135
- const sel: MultiColumnSelector = { name: [{ type: "regex", value: "vdj" as RegExpString }] };
136
- expect(matchColumn(s, sel)).toBe(false);
137
- });
138
-
139
- test("OR across name matchers", () => {
140
- const s = spec({ name: "colB" });
141
- const sel: MultiColumnSelector = {
142
- name: [
143
- { type: "exact", value: "colA" },
144
- { type: "exact", value: "colB" },
145
- ],
146
- };
147
- expect(matchColumn(s, sel)).toBe(true);
148
- });
149
- });
150
-
151
- describe("type matching", () => {
152
- test("single type match", () => {
153
- const s = spec({ name: "c", valueType: "Int" });
154
- const sel: MultiColumnSelector = { type: ["Int"] };
155
- expect(matchColumn(s, sel)).toBe(true);
156
- });
157
-
158
- test("type mismatch", () => {
159
- const s = spec({ name: "c", valueType: "String" });
160
- const sel: MultiColumnSelector = { type: ["Int"] };
161
- expect(matchColumn(s, sel)).toBe(false);
162
- });
163
-
164
- test("OR across types", () => {
165
- const s = spec({ name: "c", valueType: "Long" });
166
- const sel: MultiColumnSelector = { type: ["Int", "Long"] };
167
- expect(matchColumn(s, sel)).toBe(true);
168
- });
169
- });
170
-
171
- describe("domain matching", () => {
172
- test("matches column domain", () => {
173
- const s = spec({ name: "c", domain: { chain: "IGHeavy" } });
174
- const sel: MultiColumnSelector = {
175
- domain: { chain: [{ type: "exact", value: "IGHeavy" }] },
176
- };
177
- expect(matchColumn(s, sel)).toBe(true);
178
- });
179
-
180
- test("matches combined domain from axes", () => {
181
- const s = spec({
182
- name: "c",
183
- axesSpec: [
184
- { name: "axis1", type: "String", domain: { chain: "IGHeavy" }, annotations: {} },
185
- ] as PColumnSpec["axesSpec"],
186
- });
187
- const sel: MultiColumnSelector = {
188
- domain: { chain: [{ type: "exact", value: "IGHeavy" }] },
189
- };
190
- expect(matchColumn(s, sel)).toBe(true);
191
- });
192
-
193
- test("domain key missing fails", () => {
194
- const s = spec({ name: "c", domain: {} });
195
- const sel: MultiColumnSelector = {
196
- domain: { chain: [{ type: "exact", value: "IGHeavy" }] },
197
- };
198
- expect(matchColumn(s, sel)).toBe(false);
199
- });
200
-
201
- test("multiple domain keys AND-ed", () => {
202
- const s = spec({ name: "c", domain: { chain: "IGHeavy", species: "human" } });
203
- const sel: MultiColumnSelector = {
204
- domain: {
205
- chain: [{ type: "exact", value: "IGHeavy" }],
206
- species: [{ type: "exact", value: "human" }],
207
- },
208
- };
209
- expect(matchColumn(s, sel)).toBe(true);
210
- });
211
-
212
- test("one domain key mismatch fails", () => {
213
- const s = spec({ name: "c", domain: { chain: "IGHeavy", species: "mouse" } });
214
- const sel: MultiColumnSelector = {
215
- domain: {
216
- chain: [{ type: "exact", value: "IGHeavy" }],
217
- species: [{ type: "exact", value: "human" }],
218
- },
219
- };
220
- expect(matchColumn(s, sel)).toBe(false);
221
- });
222
- });
223
-
224
- describe("annotations matching", () => {
225
- test("exact annotation match", () => {
226
- const s = spec({ name: "c", annotations: { label: "CDR3" } });
227
- const sel: MultiColumnSelector = {
228
- annotations: { label: [{ type: "exact", value: "CDR3" }] },
229
- };
230
- expect(matchColumn(s, sel)).toBe(true);
231
- });
232
-
233
- test("regex annotation match", () => {
234
- const s = spec({ name: "c", annotations: { label: "CDR3-length" } });
235
- const sel: MultiColumnSelector = {
236
- annotations: { label: [{ type: "regex", value: ".*CDR3.*" as RegExpString }] },
237
- };
238
- expect(matchColumn(s, sel)).toBe(true);
239
- });
240
-
241
- test("missing annotation key fails", () => {
242
- const s = spec({ name: "c", annotations: {} });
243
- const sel: MultiColumnSelector = {
244
- annotations: { label: [{ type: "exact", value: "CDR3" }] },
245
- };
246
- expect(matchColumn(s, sel)).toBe(false);
247
- });
248
- });
249
-
250
- describe("axes matching", () => {
251
- const withAxes = (axes: PColumnSpec["axesSpec"]) => spec({ name: "c", axesSpec: axes });
252
-
253
- const axis = (name: string, type: string = "String") =>
254
- ({ name, type, domain: {}, annotations: {} }) as PColumnSpec["axesSpec"][number];
255
-
256
- test("partial match (default) — selector axis found", () => {
257
- const s = withAxes([axis("a1"), axis("a2")]);
258
- const sel: MultiColumnSelector = {
259
- axes: [{ name: [{ type: "exact", value: "a1" }] }],
260
- };
261
- expect(matchColumn(s, sel)).toBe(true);
262
- });
263
-
264
- test("partial match — selector axis not found", () => {
265
- const s = withAxes([axis("a1")]);
266
- const sel: MultiColumnSelector = {
267
- axes: [{ name: [{ type: "exact", value: "missing" }] }],
268
- };
269
- expect(matchColumn(s, sel)).toBe(false);
270
- });
271
-
272
- test("exact match — same count and order", () => {
273
- const s = withAxes([axis("a1"), axis("a2")]);
274
- const sel: MultiColumnSelector = {
275
- axes: [
276
- { name: [{ type: "exact", value: "a1" }] },
277
- { name: [{ type: "exact", value: "a2" }] },
278
- ],
279
- partialAxesMatch: false,
280
- };
281
- expect(matchColumn(s, sel)).toBe(true);
282
- });
283
-
284
- test("exact match — wrong order fails", () => {
285
- const s = withAxes([axis("a1"), axis("a2")]);
286
- const sel: MultiColumnSelector = {
287
- axes: [
288
- { name: [{ type: "exact", value: "a2" }] },
289
- { name: [{ type: "exact", value: "a1" }] },
290
- ],
291
- partialAxesMatch: false,
292
- };
293
- expect(matchColumn(s, sel)).toBe(false);
294
- });
295
-
296
- test("exact match — count mismatch fails", () => {
297
- const s = withAxes([axis("a1"), axis("a2")]);
298
- const sel: MultiColumnSelector = {
299
- axes: [{ name: [{ type: "exact", value: "a1" }] }],
300
- partialAxesMatch: false,
301
- };
302
- expect(matchColumn(s, sel)).toBe(false);
303
- });
304
-
305
- test("axis type matching", () => {
306
- const s = withAxes([axis("a1", "Int")]);
307
- const sel: MultiColumnSelector = {
308
- axes: [{ type: ["Int"] }],
309
- };
310
- expect(matchColumn(s, sel)).toBe(true);
311
- });
312
-
313
- test("axis domain matching", () => {
314
- const a = {
315
- name: "a1",
316
- type: "String",
317
- domain: { k: "v" },
318
- annotations: {},
319
- } as PColumnSpec["axesSpec"][number];
320
- const s = withAxes([a]);
321
- const sel: MultiColumnSelector = {
322
- axes: [{ domain: { k: [{ type: "exact", value: "v" }] } }],
323
- };
324
- expect(matchColumn(s, sel)).toBe(true);
325
- });
326
- });
327
-
328
- describe("AND across fields", () => {
329
- test("all fields must match", () => {
330
- const s = spec({
331
- name: "col1",
332
- valueType: "Int",
333
- annotations: { label: "x" },
334
- });
335
- const sel: MultiColumnSelector = {
336
- name: [{ type: "exact", value: "col1" }],
337
- type: ["Int"],
338
- annotations: { label: [{ type: "exact", value: "x" }] },
339
- };
340
- expect(matchColumn(s, sel)).toBe(true);
341
- });
342
-
343
- test("one field mismatch fails entire selector", () => {
344
- const s = spec({
345
- name: "col1",
346
- valueType: "String",
347
- });
348
- const sel: MultiColumnSelector = {
349
- name: [{ type: "exact", value: "col1" }],
350
- type: ["Int"],
351
- };
352
- expect(matchColumn(s, sel)).toBe(false);
353
- });
354
- });
355
-
356
- test("empty selector matches everything", () => {
357
- const s = spec({ name: "anything", valueType: "Double" });
358
- expect(matchColumn(s, {})).toBe(true);
359
- });
360
- });
361
-
362
- // --- matchColumnSelectors (OR across array) ---
363
-
364
- describe("matchColumnSelectors", () => {
365
- test("matches if any selector matches", () => {
366
- const s = spec({ name: "col2" });
367
- const selectors: MultiColumnSelector[] = [
368
- { name: [{ type: "exact", value: "col1" }] },
369
- { name: [{ type: "exact", value: "col2" }] },
370
- ];
371
- expect(matchColumnSelectors(selectors, s)).toBe(true);
372
- });
373
-
374
- test("no match if none match", () => {
375
- const s = spec({ name: "col3" });
376
- const selectors: MultiColumnSelector[] = [
377
- { name: [{ type: "exact", value: "col1" }] },
378
- { name: [{ type: "exact", value: "col2" }] },
379
- ];
380
- expect(matchColumnSelectors(selectors, s)).toBe(false);
381
- });
382
-
383
- test("empty array matches nothing", () => {
384
- const s = spec({ name: "col1" });
385
- expect(matchColumnSelectors([], s)).toBe(false);
386
- });
387
- });
388
-
389
- // --- columnSelectorsToPredicate ---
390
-
391
- describe("columnSelectorsToPredicate", () => {
392
- test("works with relaxed single selector", () => {
393
- const pred = columnSelectorsToPredicate({ name: "col1" });
394
- expect(pred(spec({ name: "col1" }))).toBe(true);
395
- expect(pred(spec({ name: "col2" }))).toBe(false);
396
- });
397
-
398
- test("works with relaxed array of selectors", () => {
399
- const pred = columnSelectorsToPredicate([{ name: "col1" }, { name: "col2" }]);
400
- expect(pred(spec({ name: "col1" }))).toBe(true);
401
- expect(pred(spec({ name: "col2" }))).toBe(true);
402
- expect(pred(spec({ name: "col3" }))).toBe(false);
403
- });
404
-
405
- test("works with regex in relaxed form", () => {
406
- const pred = columnSelectorsToPredicate({
407
- name: { type: "regex", value: "col[12]" as RegExpString },
408
- });
409
- expect(pred(spec({ name: "col1" }))).toBe(true);
410
- expect(pred(spec({ name: "col2" }))).toBe(true);
411
- expect(pred(spec({ name: "col3" }))).toBe(false);
412
- });
413
-
414
- test("domain filter in relaxed form", () => {
415
- const pred = columnSelectorsToPredicate({
416
- domain: { chain: "IGHeavy" },
417
- });
418
- expect(pred(spec({ name: "c", domain: { chain: "IGHeavy" } }))).toBe(true);
419
- expect(pred(spec({ name: "c", domain: { chain: "IGLight" } }))).toBe(false);
420
- expect(pred(spec({ name: "c" }))).toBe(false);
421
- });
422
- });
423
-
424
- // --- Integration: relaxed selectors end-to-end ---
425
-
426
- describe("relaxed selectors end-to-end", () => {
427
- test("design doc example: name + domain relaxed", () => {
428
- const input: RelaxedColumnSelector = {
429
- name: "pl7.app/vdj/sequence",
430
- domain: { "pl7.app/vdj/chain": "IGHeavy" },
431
- };
432
- const [sel] = normalizeSelectors(input);
433
- const s = spec({
434
- name: "pl7.app/vdj/sequence",
435
- domain: { "pl7.app/vdj/chain": "IGHeavy" },
436
- });
437
- expect(matchColumn(s, sel)).toBe(true);
438
- });
439
-
440
- test("design doc example: two selectors OR-ed", () => {
441
- const input: RelaxedColumnSelector[] = [
442
- { name: "pl7.app/vdj/sequence" },
443
- { name: "pl7.app/vdj/geneHit", domain: { "pl7.app/vdj/chain": "IGHeavy" } },
444
- ];
445
- const selectors = normalizeSelectors(input);
446
-
447
- expect(matchColumnSelectors(selectors, spec({ name: "pl7.app/vdj/sequence" }))).toBe(true);
448
-
449
- expect(
450
- matchColumnSelectors(
451
- selectors,
452
- spec({ name: "pl7.app/vdj/geneHit", domain: { "pl7.app/vdj/chain": "IGHeavy" } }),
453
- ),
454
- ).toBe(true);
455
-
456
- expect(
457
- matchColumnSelectors(
458
- selectors,
459
- spec({ name: "pl7.app/vdj/geneHit", domain: { "pl7.app/vdj/chain": "IGLight" } }),
460
- ),
461
- ).toBe(false);
462
- });
463
-
464
- test("design doc example: domain key with multiple values", () => {
465
- const pred = columnSelectorsToPredicate({
466
- domain: { "pl7.app/vdj/chain": ["IGHeavy", "IGLight"] },
467
- });
468
- expect(pred(spec({ name: "c", domain: { "pl7.app/vdj/chain": "IGHeavy" } }))).toBe(true);
469
- expect(pred(spec({ name: "c", domain: { "pl7.app/vdj/chain": "IGLight" } }))).toBe(true);
470
- expect(pred(spec({ name: "c", domain: { "pl7.app/vdj/chain": "TRA" } }))).toBe(false);
471
- });
472
- });
@@ -3,7 +3,6 @@ import type {
3
3
  ColumnValueType,
4
4
  MultiAxisSelector,
5
5
  MultiColumnSelector,
6
- PColumnSpec,
7
6
  StringMatcher,
8
7
  } from "@milaboratories/pl-model-common";
9
8
 
@@ -38,7 +37,7 @@ export interface RelaxedColumnSelector {
38
37
  }
39
38
 
40
39
  /** Input that normalizes to ColumnSelector[]. */
41
- export type ColumnSelectorInput = RelaxedColumnSelector | RelaxedColumnSelector[];
40
+ export type ColumnSelector = RelaxedColumnSelector | RelaxedColumnSelector[];
42
41
 
43
42
  // --- Normalization ---
44
43
 
@@ -64,7 +63,9 @@ function normalizeTypes<T>(input: T | T[]): T[] {
64
63
 
65
64
  type Mutable<T> = { -readonly [K in keyof T]: T[K] };
66
65
 
67
- function normalizeAxisSelector(input: RelaxedAxisSelector): MultiAxisSelector {
66
+ export function convertRelaxedAxisSelectorToMultiAxisSelector(
67
+ input: RelaxedAxisSelector,
68
+ ): MultiAxisSelector {
68
69
  const result: Mutable<MultiAxisSelector> = {};
69
70
  if (input.name !== undefined) result.name = normalizeStringMatchers(input.name);
70
71
  if (input.type !== undefined) result.type = normalizeTypes(input.type);
@@ -75,13 +76,9 @@ function normalizeAxisSelector(input: RelaxedAxisSelector): MultiAxisSelector {
75
76
  return result;
76
77
  }
77
78
 
78
- /** Normalize relaxed input to strict ColumnSelector[]. */
79
- export function normalizeSelectors(input: ColumnSelectorInput): MultiColumnSelector[] {
80
- const arr = Array.isArray(input) ? input : [input];
81
- return arr.map(normalizeSingleSelector);
82
- }
83
-
84
- function normalizeSingleSelector(input: RelaxedColumnSelector): MultiColumnSelector {
79
+ export function convertRelaxedColumnSelectorToMultiColumnSelector(
80
+ input: RelaxedColumnSelector,
81
+ ): MultiColumnSelector {
85
82
  const result: Mutable<MultiColumnSelector> = {};
86
83
  if (input.name !== undefined) result.name = normalizeStringMatchers(input.name);
87
84
  if (input.type !== undefined) result.type = normalizeTypes(input.type);
@@ -89,125 +86,15 @@ function normalizeSingleSelector(input: RelaxedColumnSelector): MultiColumnSelec
89
86
  if (input.contextDomain !== undefined)
90
87
  result.contextDomain = normalizeRecord(input.contextDomain);
91
88
  if (input.annotations !== undefined) result.annotations = normalizeRecord(input.annotations);
92
- if (input.axes !== undefined) result.axes = input.axes.map(normalizeAxisSelector);
89
+ if (input.axes !== undefined)
90
+ result.axes = input.axes.map(convertRelaxedAxisSelectorToMultiAxisSelector);
93
91
  if (input.partialAxesMatch !== undefined) result.partialAxesMatch = input.partialAxesMatch;
94
92
  return result;
95
93
  }
96
94
 
97
- // --- Matching ---
98
-
99
- function matchStringValue(value: string, matchers: StringMatcher[]): boolean {
100
- return matchers.some((m) => {
101
- if (m.type === "exact") return value === m.value;
102
- return new RegExp(`^(?:${m.value})$`).test(value);
103
- });
104
- }
105
-
106
- function matchRecordField(
107
- actual: Record<string, string> | undefined,
108
- required: Record<string, StringMatcher[]>,
109
- ): boolean {
110
- const record = actual ?? {};
111
- for (const [key, matchers] of Object.entries(required)) {
112
- const value = record[key];
113
- if (value === undefined) return false;
114
- if (!matchStringValue(value, matchers)) return false;
115
- }
116
- return true;
117
- }
118
-
119
- /** Get combined domain: column's own domain merged with all axis domains. */
120
- function getCombinedDomain(spec: PColumnSpec): Record<string, string> {
121
- const result: Record<string, string> = {};
122
- if (spec.domain) Object.assign(result, spec.domain);
123
- for (const axis of spec.axesSpec) {
124
- if (axis.domain) Object.assign(result, axis.domain);
125
- }
126
- return result;
127
- }
128
-
129
- /** Get combined context domain: column's own contextDomain merged with all axis contextDomains. */
130
- function getCombinedContextDomain(spec: PColumnSpec): Record<string, string> {
131
- const result: Record<string, string> = {};
132
- if (spec.contextDomain) Object.assign(result, spec.contextDomain);
133
- for (const axis of spec.axesSpec) {
134
- if ("contextDomain" in axis && axis.contextDomain) Object.assign(result, axis.contextDomain);
135
- }
136
- return result;
137
- }
138
-
139
- function matchAxisSelector(
140
- axis: PColumnSpec["axesSpec"][number],
141
- selector: MultiAxisSelector,
142
- ): boolean {
143
- if (selector.name !== undefined && !matchStringValue(axis.name, selector.name)) return false;
144
- if (selector.type !== undefined && !selector.type.includes(axis.type)) return false;
145
- if (selector.domain !== undefined && !matchRecordField(axis.domain, selector.domain))
146
- return false;
147
- if (
148
- selector.contextDomain !== undefined &&
149
- !matchRecordField(
150
- "contextDomain" in axis ? (axis.contextDomain as Record<string, string>) : undefined,
151
- selector.contextDomain,
152
- )
153
- )
154
- return false;
155
- if (
156
- selector.annotations !== undefined &&
157
- !matchRecordField(axis.annotations, selector.annotations)
158
- )
159
- return false;
160
- return true;
161
- }
162
-
163
- /** Check if a PColumnSpec matches a single strict ColumnSelector. */
164
- export function matchColumn(spec: PColumnSpec, selector: MultiColumnSelector): boolean {
165
- if (selector.name !== undefined && !matchStringValue(spec.name, selector.name)) return false;
166
- if (selector.type !== undefined && !selector.type.includes(spec.valueType)) return false;
167
-
168
- if (selector.domain !== undefined) {
169
- const combined = getCombinedDomain(spec);
170
- if (!matchRecordField(combined, selector.domain)) return false;
171
- }
172
-
173
- if (selector.contextDomain !== undefined) {
174
- const combined = getCombinedContextDomain(spec);
175
- if (!matchRecordField(combined, selector.contextDomain)) return false;
176
- }
177
-
178
- if (selector.annotations !== undefined) {
179
- if (!matchRecordField(spec.annotations, selector.annotations)) return false;
180
- }
181
-
182
- if (selector.axes !== undefined) {
183
- const partialMatch = selector.partialAxesMatch ?? true;
184
- if (partialMatch) {
185
- for (const axisSel of selector.axes) {
186
- if (!spec.axesSpec.some((axis) => matchAxisSelector(axis, axisSel))) return false;
187
- }
188
- } else {
189
- if (spec.axesSpec.length !== selector.axes.length) return false;
190
- for (let i = 0; i < selector.axes.length; i++) {
191
- if (!matchAxisSelector(spec.axesSpec[i], selector.axes[i])) return false;
192
- }
193
- }
194
- }
195
-
196
- return true;
197
- }
198
-
199
- /** Check if a PColumnSpec matches any of the selectors (OR across array). */
200
- export function matchColumnSelectors(selectors: MultiColumnSelector[], spec: PColumnSpec): boolean {
201
- return selectors.some((sel) => matchColumn(spec, sel));
202
- }
203
-
204
- /**
205
- * Convert selector input to a predicate function.
206
- * Normalizes relaxed form, then returns a function that OR-matches.
207
- */
208
- export function columnSelectorsToPredicate(
209
- input: ColumnSelectorInput,
210
- ): (spec: PColumnSpec) => boolean {
211
- const selectors = normalizeSelectors(input);
212
- return (spec) => matchColumnSelectors(selectors, spec);
95
+ export function convertColumnSelectorToMultiColumnSelector(
96
+ input: ColumnSelector,
97
+ ): MultiColumnSelector[] {
98
+ const arr = Array.isArray(input) ? input : [input];
99
+ return arr.map(convertRelaxedColumnSelectorToMultiColumnSelector);
213
100
  }
@@ -1,4 +1,4 @@
1
- import type { PColumnSpec, PObjectId } from "@milaboratories/pl-model-common";
1
+ import type { PColumnSpec, PObjectId, SUniversalPColumnId } from "@milaboratories/pl-model-common";
2
2
  import type { PColumnDataUniversal } from "../render/internal";
3
3
 
4
4
  // --- ColumnSnapshot ---
@@ -13,7 +13,7 @@ export type ColumnDataStatus = "ready" | "computing" | "absent";
13
13
  * - `data` holds an active object when data exists (ready or computing),
14
14
  * or `undefined` when data is permanently absent.
15
15
  */
16
- export interface ColumnSnapshot<Id extends PObjectId = PObjectId> {
16
+ export interface ColumnSnapshot<Id extends PObjectId | SUniversalPColumnId> {
17
17
  readonly id: Id;
18
18
  readonly spec: PColumnSpec;
19
19
  readonly dataStatus: ColumnDataStatus;
@@ -45,11 +45,11 @@ export function createReadyColumnData(getData: () => PColumnDataUniversal | unde
45
45
  // --- Snapshot construction helpers ---
46
46
 
47
47
  /** Creates a ColumnSnapshot from parts. */
48
- export function createColumnSnapshot<Id extends PObjectId = PObjectId>(
48
+ export function createColumnSnapshot<Id extends PObjectId>(
49
49
  id: Id,
50
50
  spec: PColumnSpec,
51
+ data: undefined | ColumnData,
51
52
  dataStatus: ColumnDataStatus,
52
- data: ColumnData | undefined,
53
53
  ): ColumnSnapshot<Id> {
54
- return { id, spec, dataStatus, data };
54
+ return { id, spec, data, dataStatus };
55
55
  }