@platforma-sdk/model 1.80.17 → 1.81.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 (118) hide show
  1. package/dist/columns/column.cjs +9 -6
  2. package/dist/columns/column.cjs.map +1 -1
  3. package/dist/columns/column.d.ts +5 -5
  4. package/dist/columns/column.d.ts.map +1 -1
  5. package/dist/columns/column.js +10 -7
  6. package/dist/columns/column.js.map +1 -1
  7. package/dist/columns/column_providers/index.cjs.map +1 -1
  8. package/dist/columns/column_providers/index.d.ts.map +1 -1
  9. package/dist/columns/column_providers/index.js.map +1 -1
  10. package/dist/columns/column_providers/providers.cjs +5 -5
  11. package/dist/columns/column_providers/providers.cjs.map +1 -1
  12. package/dist/columns/column_providers/providers.d.ts +8 -8
  13. package/dist/columns/column_providers/providers.d.ts.map +1 -1
  14. package/dist/columns/column_providers/providers.js +5 -5
  15. package/dist/columns/column_providers/providers.js.map +1 -1
  16. package/dist/columns/column_providers/types.d.ts +3 -3
  17. package/dist/columns/column_providers/types.d.ts.map +1 -1
  18. package/dist/columns/column_recipes/column_discovered_recipe.cjs +1 -1
  19. package/dist/columns/column_recipes/column_discovered_recipe.cjs.map +1 -1
  20. package/dist/columns/column_recipes/column_discovered_recipe.d.ts +1 -1
  21. package/dist/columns/column_recipes/column_discovered_recipe.js +1 -1
  22. package/dist/columns/column_recipes/column_discovered_recipe.js.map +1 -1
  23. package/dist/columns/column_recipes/column_overrided_recipe.cjs +47 -2
  24. package/dist/columns/column_recipes/column_overrided_recipe.cjs.map +1 -1
  25. package/dist/columns/column_recipes/column_overrided_recipe.d.ts +30 -3
  26. package/dist/columns/column_recipes/column_overrided_recipe.d.ts.map +1 -1
  27. package/dist/columns/column_recipes/column_overrided_recipe.js +47 -3
  28. package/dist/columns/column_recipes/column_overrided_recipe.js.map +1 -1
  29. package/dist/columns/column_recipes/index.cjs +4 -14
  30. package/dist/columns/column_recipes/index.cjs.map +1 -1
  31. package/dist/columns/column_recipes/index.d.ts +3 -10
  32. package/dist/columns/column_recipes/index.d.ts.map +1 -1
  33. package/dist/columns/column_recipes/index.js +5 -14
  34. package/dist/columns/column_recipes/index.js.map +1 -1
  35. package/dist/columns/{column_lazy.cjs → data_column.cjs} +57 -54
  36. package/dist/columns/data_column.cjs.map +1 -0
  37. package/dist/columns/data_column.d.ts +152 -0
  38. package/dist/columns/data_column.d.ts.map +1 -0
  39. package/dist/columns/{column_lazy.js → data_column.js} +55 -52
  40. package/dist/columns/data_column.js.map +1 -0
  41. package/dist/columns/derive_axis_values_labels.cjs +4 -4
  42. package/dist/columns/derive_axis_values_labels.cjs.map +1 -1
  43. package/dist/columns/derive_axis_values_labels.d.ts +2 -2
  44. package/dist/columns/derive_axis_values_labels.js +4 -4
  45. package/dist/columns/derive_axis_values_labels.js.map +1 -1
  46. package/dist/columns/index.cjs +9 -9
  47. package/dist/columns/index.d.ts +5 -5
  48. package/dist/columns/index.js +5 -5
  49. package/dist/columns/{expand_by_partition.cjs → split_by_axes.cjs} +13 -8
  50. package/dist/columns/split_by_axes.cjs.map +1 -0
  51. package/dist/columns/{expand_by_partition.d.ts → split_by_axes.d.ts} +18 -8
  52. package/dist/columns/split_by_axes.d.ts.map +1 -0
  53. package/dist/columns/{expand_by_partition.js → split_by_axes.js} +14 -9
  54. package/dist/columns/split_by_axes.js.map +1 -0
  55. package/dist/columns/utils.cjs +50 -43
  56. package/dist/columns/utils.cjs.map +1 -1
  57. package/dist/columns/utils.d.ts +44 -19
  58. package/dist/columns/utils.d.ts.map +1 -1
  59. package/dist/columns/utils.js +50 -44
  60. package/dist/columns/utils.js.map +1 -1
  61. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +3 -3
  62. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -1
  63. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +3 -3
  64. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -1
  65. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs +2 -2
  66. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs.map +1 -1
  67. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js +2 -2
  68. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js.map +1 -1
  69. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +1 -1
  70. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  71. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +2 -2
  72. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  73. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +1 -1
  74. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  75. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +2 -2
  76. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  77. package/dist/components/PlDatasetSelector/enrichment_discovery.cjs +1 -1
  78. package/dist/components/PlDatasetSelector/enrichment_discovery.cjs.map +1 -1
  79. package/dist/components/PlDatasetSelector/enrichment_discovery.js +2 -2
  80. package/dist/components/PlDatasetSelector/enrichment_discovery.js.map +1 -1
  81. package/dist/index.cjs +9 -9
  82. package/dist/index.d.ts +5 -5
  83. package/dist/index.js +5 -5
  84. package/dist/package.cjs +1 -1
  85. package/dist/package.js +1 -1
  86. package/package.json +7 -7
  87. package/src/columns/__test_helpers__/collection_driver.ts +6 -6
  88. package/src/columns/__test_helpers__/stub_registry.ts +4 -6
  89. package/src/columns/column.ts +20 -9
  90. package/src/columns/column_providers/index.ts +9 -3
  91. package/src/columns/column_providers/providers.ts +16 -15
  92. package/src/columns/column_providers/types.ts +3 -3
  93. package/src/columns/column_recipes/column_discovered_recipe.ts +1 -1
  94. package/src/columns/column_recipes/column_overrided_recipe.ts +58 -4
  95. package/src/columns/column_recipes/column_recipes.test.ts +2 -2
  96. package/src/columns/column_recipes/index.ts +5 -20
  97. package/src/columns/{column_lazy.test.ts → data_column.test.ts} +19 -19
  98. package/src/columns/{column_lazy.ts → data_column.ts} +105 -67
  99. package/src/columns/derive_axis_values_labels.ts +4 -4
  100. package/src/columns/index.ts +2 -2
  101. package/src/columns/predicates.test.ts +124 -0
  102. package/src/columns/{expand_by_partition.test.ts → split_by_axes.test.ts} +29 -21
  103. package/src/columns/{expand_by_partition.ts → split_by_axes.ts} +22 -9
  104. package/src/columns/utils.ts +53 -48
  105. package/src/components/PlDataTable/columnResolver.test.ts +3 -3
  106. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +3 -3
  107. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV2.ts +2 -2
  108. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +2 -2
  109. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +2 -2
  110. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +3 -3
  111. package/src/components/PlDatasetSelector/enrichment_discovery.ts +2 -2
  112. package/dist/columns/column_lazy.cjs.map +0 -1
  113. package/dist/columns/column_lazy.d.ts +0 -137
  114. package/dist/columns/column_lazy.d.ts.map +0 -1
  115. package/dist/columns/column_lazy.js.map +0 -1
  116. package/dist/columns/expand_by_partition.cjs.map +0 -1
  117. package/dist/columns/expand_by_partition.d.ts.map +0 -1
  118. package/dist/columns/expand_by_partition.js.map +0 -1
@@ -8,7 +8,7 @@ import {
8
8
  isColumnFilteredKey,
9
9
  } from "@milaboratories/pl-model-common";
10
10
  import type { GlobalCfgRenderCtx } from "../../render/internal";
11
- import { ColumnLazyImpl } from "../column_lazy";
11
+ import { DataColumn } from "../data_column";
12
12
  import { ColumnDiscoveredRecipe } from "./column_discovered_recipe";
13
13
  import { ColumnFilteredRecipe } from "./column_filtered_recipe";
14
14
  import { ColumnOverriddenRecipe } from "./column_overrided_recipe";
@@ -19,7 +19,7 @@ import type {
19
19
  } from "./types";
20
20
 
21
21
  export type { ColumnFieldStatus, ColumnRecipeId, ColumnResolutionStatus, SpecQuery } from "./types";
22
- export { ColumnAbsentError } from "../column_lazy";
22
+ export { ColumnAbsentError } from "../data_column";
23
23
  export { ColumnDiscoveredRecipe } from "./column_discovered_recipe";
24
24
  export { ColumnFilteredRecipe } from "./column_filtered_recipe";
25
25
  export { ColumnOverriddenRecipe } from "./column_overrided_recipe";
@@ -38,7 +38,7 @@ export interface ColumnRecipe<
38
38
  * concrete recipe variant based on the id's encoding and recurses on
39
39
  * wrappers' inner `source`:
40
40
  *
41
- * - bare {@link PObjectId} (non-JSON) → `ColumnLazy.fromId`
41
+ * - bare {@link PObjectId} (non-JSON) → `DataColumnRecipe.fromId`
42
42
  * - `ColumnDiscoveredKey` → {@link ColumnDiscoveredRecipe}
43
43
  * - `ColumnOverriddenKey { source, specOverrides }` → recurse on `source`,
44
44
  * then {@link ColumnOverriddenRecipe.wrap}
@@ -56,7 +56,7 @@ function ColumnRecipeBuild(
56
56
  const parsed = parseColumnIdSafely(id);
57
57
 
58
58
  if (isPObjectKey(parsed)) {
59
- return ColumnLazyImpl.fromId(id as PObjectId, opts);
59
+ return DataColumn.fromId(id as PObjectId, opts);
60
60
  }
61
61
 
62
62
  if (isColumnDiscoveredKey(parsed)) {
@@ -98,7 +98,7 @@ function ColumnRecipeGetStatus(
98
98
  opts: { ctx?: GlobalCfgRenderCtx } = {},
99
99
  ): ColumnResolutionStatus {
100
100
  const parsed = parseColumnIdSafely(id);
101
- if (isPObjectKey(parsed)) return ColumnLazyImpl.getStatusById(id as PObjectId, opts);
101
+ if (isPObjectKey(parsed)) return DataColumn.getStatusById(id as PObjectId, opts);
102
102
  if (isColumnDiscoveredKey(parsed)) return ColumnDiscoveredRecipe.getStatusByKey(parsed, opts);
103
103
  if (isColumnOverriddenKey(parsed)) return ColumnOverriddenRecipe.getStatusByKey(parsed, opts);
104
104
  if (isColumnFilteredKey(parsed)) return ColumnFilteredRecipe.getStatusByKey(parsed, opts);
@@ -110,18 +110,3 @@ export const ColumnRecipe: typeof ColumnRecipeBuild & {
110
110
  } = Object.assign(ColumnRecipeBuild, {
111
111
  getStatus: ColumnRecipeGetStatus,
112
112
  });
113
-
114
- /**
115
- * Type-guard for any recipe class — leaf {@link ColumnLazyImpl} or any of the
116
- * wrapper recipes ({@link ColumnDiscoveredRecipe}, {@link ColumnFilteredRecipe},
117
- * {@link ColumnOverriddenRecipe}). Use when a value may be a raw id, a `PColumn`,
118
- * or a recipe and you want to branch on the recipe case.
119
- */
120
- export function isColumnRecipe(value: unknown): value is ColumnRecipe {
121
- return (
122
- value instanceof ColumnLazyImpl ||
123
- value instanceof ColumnDiscoveredRecipe ||
124
- value instanceof ColumnFilteredRecipe ||
125
- value instanceof ColumnOverriddenRecipe
126
- );
127
- }
@@ -6,12 +6,12 @@ import {
6
6
  type PColumnSpec,
7
7
  type PObjectId,
8
8
  } from "@milaboratories/pl-model-common";
9
- import { ColumnAbsentError, ColumnLazy, ColumnLazyImpl } from "./column_lazy";
9
+ import { ColumnAbsentError, DataColumnImpl } from "./data_column";
10
10
  import { _ctxProvidersCache } from "./column_providers";
11
11
  import { installStubRegistry } from "./__test_helpers__/stub_registry";
12
12
 
13
13
  // --- Ambient ctx mock ------------------------------------------------------
14
- // ColumnLazy constructor pulls `getCfgRenderCtx()` from globalThis. A minimal
14
+ // DataColumnImpl constructor pulls `getCfgRenderCtx()` from globalThis. A minimal
15
15
  // mock with empty index is enough — these tests don't exercise resolve paths.
16
16
 
17
17
  const mockCtx = {
@@ -34,12 +34,12 @@ const baseLeaf = createLocalPObjectId(["main", "out"], "col-1");
34
34
  const stubSpec = { kind: "PColumn", name: "stub" } as unknown as PColumnSpec;
35
35
 
36
36
  function lazyOf(id: PObjectId, spec: PColumnSpec = stubSpec) {
37
- return ColumnLazyImpl.fromColumn({ id, spec, data: undefined });
37
+ return DataColumnImpl.fromColumn({ id, spec, data: undefined });
38
38
  }
39
39
 
40
40
  // --- clone flattening -------------------------------------------------------
41
41
 
42
- describe("ColumnLazy.clone", () => {
42
+ describe("DataColumnImpl.clone", () => {
43
43
  test("two successive clones produce flat wrap (no nesting)", () => {
44
44
  const c0 = lazyOf(baseLeaf);
45
45
  const c1 = c0.withSpecs({
@@ -77,7 +77,7 @@ describe("ColumnLazy.clone", () => {
77
77
 
78
78
  // --- spec() override application -------------------------------------------
79
79
 
80
- describe("ColumnLazy.spec — applySpecOverrides", () => {
80
+ describe("DataColumnImpl.spec — applySpecOverrides", () => {
81
81
  test("base spec with no overrides returns readers.spec verbatim", () => {
82
82
  const c = lazyOf(baseLeaf);
83
83
  expect(c.getSpec()).toBe(stubSpec);
@@ -86,7 +86,7 @@ describe("ColumnLazy.spec — applySpecOverrides", () => {
86
86
  test("fromId throws ColumnAbsentError when ctx has no handles", () => {
87
87
  // Empty registry — `isFinal()` returns true vacuously, so the leaf is
88
88
  // provably absent (no provider will ever supply it).
89
- expect(() => ColumnLazyImpl.fromId(baseLeaf)).toThrow(ColumnAbsentError);
89
+ expect(() => DataColumnImpl.fromId(baseLeaf)).toThrow(ColumnAbsentError);
90
90
  });
91
91
 
92
92
  test("appends axes at indices past base.axesSpec.length", () => {
@@ -129,67 +129,67 @@ describe("ColumnLazy.spec — applySpecOverrides", () => {
129
129
 
130
130
  // --- resolution status / absent throw --------------------------------------
131
131
 
132
- describe("ColumnLazy resolution: fromId / fromAccessor / getStatus", () => {
132
+ describe("DataColumnImpl resolution: fromId / fromAccessor / getStatus", () => {
133
133
  test("fromId returns undefined when registry is unfinalized and id not found", () => {
134
134
  // `isFinal: false` → resolver can't tell yet → resolving → undefined.
135
135
  installStubRegistry(mockCtx, {}, { isFinal: false });
136
- expect(ColumnLazyImpl.fromId(baseLeaf)).toBeUndefined();
136
+ expect(DataColumnImpl.fromId(baseLeaf)).toBeUndefined();
137
137
  });
138
138
 
139
139
  test("fromId throws ColumnAbsentError when leaf has no spec field on locked accessor", () => {
140
140
  // Leaf entry present, no spec field, accessor locked → absent forever.
141
141
  installStubRegistry(mockCtx, { [baseLeaf]: { accessorLocked: true } });
142
- expect(() => ColumnLazyImpl.fromId(baseLeaf)).toThrow(ColumnAbsentError);
142
+ expect(() => DataColumnImpl.fromId(baseLeaf)).toThrow(ColumnAbsentError);
143
143
  });
144
144
 
145
145
  test("fromId returns undefined when leaf has no spec field on unlocked accessor", () => {
146
146
  // Leaf entry present, no spec field, accessor unlocked → spec may still arrive.
147
147
  installStubRegistry(mockCtx, { [baseLeaf]: { accessorLocked: false } });
148
- expect(ColumnLazyImpl.fromId(baseLeaf)).toBeUndefined();
148
+ expect(DataColumnImpl.fromId(baseLeaf)).toBeUndefined();
149
149
  });
150
150
 
151
151
  test("fromId returns undefined when spec accessor exists but hasData is false", () => {
152
152
  // Spec resource present, bytes not yet written — transient resolving.
153
153
  installStubRegistry(mockCtx, { [baseLeaf]: { spec: stubSpec, specHasData: false } });
154
- expect(ColumnLazyImpl.fromId(baseLeaf)).toBeUndefined();
154
+ expect(DataColumnImpl.fromId(baseLeaf)).toBeUndefined();
155
155
  });
156
156
 
157
157
  test("fromAccessor throws ColumnAbsentError when accessor is locked without spec", () => {
158
158
  installStubRegistry(mockCtx, { [baseLeaf]: { accessorLocked: true } });
159
159
  const entry = pluckLeafEntry(baseLeaf);
160
- expect(() => ColumnLazyImpl.fromAccessor(entry)).toThrow(ColumnAbsentError);
160
+ expect(() => DataColumnImpl.fromAccessor(entry)).toThrow(ColumnAbsentError);
161
161
  });
162
162
 
163
163
  test("fromAccessor returns undefined when accessor is unlocked without spec", () => {
164
164
  installStubRegistry(mockCtx, { [baseLeaf]: { accessorLocked: false } });
165
165
  const entry = pluckLeafEntry(baseLeaf);
166
- expect(ColumnLazyImpl.fromAccessor(entry)).toBeUndefined();
166
+ expect(DataColumnImpl.fromAccessor(entry)).toBeUndefined();
167
167
  });
168
168
 
169
169
  test("getStatusById returns 'absent' / 'resolving' / 'present' across configurations", () => {
170
170
  // present — spec with data on a locked, populated leaf.
171
171
  installStubRegistry(mockCtx, { [baseLeaf]: stubSpec });
172
- expect(ColumnLazy.getStatusById(baseLeaf)).toBe("present");
172
+ expect(DataColumnImpl.getStatusById(baseLeaf)).toBe("present");
173
173
 
174
174
  // resolving — same leaf id, spec bytes not yet written.
175
175
  installStubRegistry(mockCtx, { [baseLeaf]: { spec: stubSpec, specHasData: false } });
176
- expect(ColumnLazy.getStatusById(baseLeaf)).toBe("resolving");
176
+ expect(DataColumnImpl.getStatusById(baseLeaf)).toBe("resolving");
177
177
 
178
178
  // resolving — leaf entry exists but spec field missing on an unlocked accessor.
179
179
  installStubRegistry(mockCtx, { [baseLeaf]: { accessorLocked: false } });
180
- expect(ColumnLazy.getStatusById(baseLeaf)).toBe("resolving");
180
+ expect(DataColumnImpl.getStatusById(baseLeaf)).toBe("resolving");
181
181
 
182
182
  // absent — leaf entry exists with no spec field on a locked accessor.
183
183
  installStubRegistry(mockCtx, { [baseLeaf]: { accessorLocked: true } });
184
- expect(ColumnLazy.getStatusById(baseLeaf)).toBe("absent");
184
+ expect(DataColumnImpl.getStatusById(baseLeaf)).toBe("absent");
185
185
 
186
186
  // absent — id not in any provider and registry is final.
187
187
  installStubRegistry(mockCtx, {}, { isFinal: true });
188
- expect(ColumnLazy.getStatusById(baseLeaf)).toBe("absent");
188
+ expect(DataColumnImpl.getStatusById(baseLeaf)).toBe("absent");
189
189
 
190
190
  // resolving — id not in any provider but registry still enumerating.
191
191
  installStubRegistry(mockCtx, {}, { isFinal: false });
192
- expect(ColumnLazy.getStatusById(baseLeaf)).toBe("resolving");
192
+ expect(DataColumnImpl.getStatusById(baseLeaf)).toBe("resolving");
193
193
  });
194
194
  });
195
195
 
@@ -20,14 +20,37 @@ import { TreeNodeAccessor } from "../render";
20
20
  import type {
21
21
  ColumnFieldStatus,
22
22
  ColumnRecipe,
23
+ ColumnRecipeId,
23
24
  ColumnResolutionStatus,
24
25
  } from "./column_recipes/types";
25
26
  import { ColumnOverriddenRecipe } from "./column_recipes/column_overrided_recipe";
26
27
 
27
- export type ColumnLazyId = PObjectId;
28
- export type ColumnLazyData = undefined | PColumnDataUniversal;
28
+ export type DataColumnId = PObjectId;
29
+ export type ColumnData = undefined | PColumnDataUniversal;
29
30
  export type { ColumnFieldStatus, ColumnResolutionStatus } from "./column_recipes/types";
30
31
 
32
+ /**
33
+ * A {@link ColumnRecipe} that can be read directly in the sandbox: its
34
+ * {@link getData} is guaranteed to be consistent with {@link getSpec}.
35
+ *
36
+ * This is a *capability*, not a class. Only two recipe shapes carry it — a
37
+ * bare leaf and a spec-override over a bare leaf — because those are the only
38
+ * ones whose data still matches their spec. An axis-filtered recipe drops axes
39
+ * from its spec while the underlying data keeps them, and a discovered recipe
40
+ * only materialises after a join; neither can be read here, so neither
41
+ * implements this interface.
42
+ *
43
+ * Narrow to it with `hasReachableData(recipe)` rather than testing for a concrete
44
+ * class — which recipe classes exist is an implementation detail behind
45
+ * `recipe.id`.
46
+ */
47
+ export interface DataColumnRecipe<
48
+ ID extends ColumnRecipeId = ColumnRecipeId,
49
+ > extends ColumnRecipe<ID> {
50
+ /** Column data, consistent with {@link ColumnRecipe.getSpec}. */
51
+ getData(): ColumnData;
52
+ }
53
+
31
54
  /**
32
55
  * Thrown by leaf-recipe factories when the requested column is provably
33
56
  * absent in the active render ctx — i.e. every relevant accessor reports
@@ -47,22 +70,24 @@ export class ColumnAbsentError extends Error {
47
70
  }
48
71
 
49
72
  /**
50
- * ColumnLazy is the leaf-recipe building block: a {@link ColumnRecipe} whose
51
- * `id` is a bare {@link PObjectId} and whose readers are bound to a single
52
- * tree-accessor leaf. Layered encodings (Overridden / Discovered / Filtered)
53
- * are reified through their dedicated recipe classes and reference leaf
54
- * columns by id.
73
+ * The leaf-recipe building block: a {@link DataColumnRecipe} whose `id` is a bare
74
+ * {@link PObjectId} and whose readers are bound to a single tree-accessor
75
+ * leaf. Layered encodings (Overridden / Discovered / Filtered) are reified
76
+ * through their dedicated recipe classes and reference leaf columns by id.
77
+ *
78
+ * Internal — consumers hold the {@link DataColumnRecipe} interface and construct
79
+ * through the {@link DataColumnRecipe} dispatcher.
55
80
  */
56
- export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
81
+ export class DataColumnImpl implements DataColumnRecipe<PObjectId> {
57
82
  private specCache?: { readonly value: PColumnSpec };
58
- private dataCache?: { readonly value: ColumnLazyData };
83
+ private dataCache?: { readonly value: ColumnData };
59
84
  private dataStatusCache?: { readonly value: ColumnFieldStatus };
60
85
 
61
86
  private constructor(
62
87
  public readonly id: PObjectId,
63
88
  private readonly options: {
64
89
  getSpec: () => PColumnSpec;
65
- getData: () => ColumnLazyData;
90
+ getData: () => ColumnData;
66
91
  getDataStatus: () => ColumnFieldStatus;
67
92
  },
68
93
  ) {}
@@ -84,8 +109,7 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
84
109
  return { type: "column", column: this.id } as SpecQuery;
85
110
  }
86
111
 
87
- /** Leaf-only: not on the recipe interface — only the leaf can read data directly. */
88
- getData(): ColumnLazyData {
112
+ getData(): ColumnData {
89
113
  if (this.dataCache === undefined) this.dataCache = { value: this.options.getData() };
90
114
  return this.dataCache.value;
91
115
  }
@@ -99,7 +123,7 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
99
123
 
100
124
  /**
101
125
  * Overlay overrides → produces a {@link ColumnOverriddenRecipe} wrapping
102
- * this leaf. ColumnLazy itself stays bare (id remains a plain PObjectId);
126
+ * this leaf. The leaf itself stays bare (id remains a plain PObjectId);
103
127
  * layering lives entirely in the recipe wrappers.
104
128
  */
105
129
  withSpecs(overrides: SpecOverrides): ColumnRecipe {
@@ -113,7 +137,10 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
113
137
  * and the column did not appear — the column will not exist in this ctx.
114
138
  * Data and dataStatus stay lazy.
115
139
  */
116
- static fromId(id: PObjectId, { ctx }: { ctx?: GlobalCfgRenderCtx } = {}): undefined | ColumnLazy {
140
+ static fromId(
141
+ id: PObjectId,
142
+ { ctx }: { ctx?: GlobalCfgRenderCtx } = {},
143
+ ): undefined | DataColumnRecipe<PObjectId> {
117
144
  const registry = new ColumnRegistry(getCtxProviders({ ctx }));
118
145
  const leaf = registry.resolve(id);
119
146
  if (isNil(leaf)) {
@@ -126,7 +153,7 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
126
153
  return undefined;
127
154
  }
128
155
  if (!spec.hasData()) return undefined;
129
- return new ColumnLazyImpl(id, {
156
+ return new DataColumnImpl(id, {
130
157
  getSpec: () => spec.getDataAsJson<PColumnSpec>(),
131
158
  getData: () => readDataAccessor(leaf),
132
159
  getDataStatus: () => readDataStatus(leaf),
@@ -134,8 +161,8 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
134
161
  }
135
162
 
136
163
  /** {@link PlRef} wrapper over {@link fromId}. */
137
- static fromPlRef(ref: PlRef): undefined | ColumnLazy {
138
- return ColumnLazyImpl.fromId(createGlobalPObjectId(ref.blockId, ref.name));
164
+ static fromPlRef(ref: PlRef): undefined | DataColumnRecipe<PObjectId> {
165
+ return DataColumnImpl.fromId(createGlobalPObjectId(ref.blockId, ref.name));
139
166
  }
140
167
 
141
168
  /**
@@ -143,14 +170,14 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
143
170
  * Throws {@link ColumnAbsentError} if the leaf has no spec field and its
144
171
  * accessor is `inputsLocked`. Returns `undefined` while still resolving.
145
172
  */
146
- static fromAccessor(entry: LeafEntry<TreeNodeAccessor>): undefined | ColumnLazy {
173
+ static fromAccessor(entry: LeafEntry<TreeNodeAccessor>): undefined | DataColumnRecipe<PObjectId> {
147
174
  const spec = readSpecAccessor(entry);
148
175
  if (isNil(spec)) {
149
176
  if (entry.accessor.getInputsLocked()) throw new ColumnAbsentError(entry.id);
150
177
  return undefined;
151
178
  }
152
179
  if (!spec.hasData()) return undefined;
153
- return new ColumnLazyImpl(entry.id, {
180
+ return new DataColumnImpl(entry.id, {
154
181
  getSpec: () => spec.getDataAsJson<PColumnSpec>(),
155
182
  getData: () => readDataAccessor(entry),
156
183
  getDataStatus: () => readDataStatus(entry),
@@ -159,13 +186,20 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
159
186
 
160
187
  /**
161
188
  * Wrap a materialised {@link PColumn}. If the input is already a
162
- * {@link ColumnLazy} it is returned as-is.
189
+ * {@link DataColumnRecipe} leaf it is returned as-is.
163
190
  */
164
- static fromColumn(column: PColumn<ColumnLazyData> | ColumnLazy): ColumnLazy {
165
- if (column instanceof ColumnLazyImpl) return column;
166
- return new ColumnLazyImpl(column.id, {
167
- getSpec: () => column.spec,
168
- getData: () => column.data,
191
+ static fromColumn(
192
+ column: PColumn<ColumnData> | DataColumnRecipe<PObjectId>,
193
+ ): DataColumnRecipe<PObjectId> {
194
+ if (column instanceof DataColumnImpl) return column;
195
+ // `DataColumnRecipe` is an interface, so `instanceof` above does not remove it
196
+ // from the union. The only implementation carrying a bare `PObjectId` is
197
+ // `DataColumnImpl` (wrappers expose `ColumnUniversalId`), so anything
198
+ // reaching here is a materialised `PColumn`.
199
+ const pColumn = column as PColumn<ColumnData>;
200
+ return new DataColumnImpl(pColumn.id, {
201
+ getSpec: () => pColumn.spec,
202
+ getData: () => pColumn.data,
169
203
  getDataStatus: () => "present",
170
204
  });
171
205
  }
@@ -191,7 +225,7 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
191
225
  ref: PlRef,
192
226
  opts: { ctx?: GlobalCfgRenderCtx } = {},
193
227
  ): ColumnResolutionStatus {
194
- return ColumnLazyImpl.getStatusById(createGlobalPObjectId(ref.blockId, ref.name), opts);
228
+ return DataColumnImpl.getStatusById(createGlobalPObjectId(ref.blockId, ref.name), opts);
195
229
  }
196
230
 
197
231
  /** No registry — reads straight off the entry's accessor. */
@@ -200,67 +234,71 @@ export class ColumnLazyImpl implements ColumnRecipe<PObjectId> {
200
234
  }
201
235
  }
202
236
 
203
- /**
204
- * Public type alias — `ColumnLazy` (the type) refers to the underlying
205
- * {@link ColumnLazyImpl} class instance. `ColumnLazy` (the value) is the
206
- * callable below with the static factories attached.
207
- */
208
- export type ColumnLazy = ColumnLazyImpl;
237
+ /** Anything the {@link DataColumnRecipe} dispatcher can build a leaf from. */
238
+ export type DataColumnSource =
239
+ | PObjectId
240
+ | PlRef
241
+ | LeafEntry<TreeNodeAccessor>
242
+ | PColumn<ColumnData>
243
+ | DataColumnRecipe<PObjectId>;
209
244
 
210
245
  /**
211
- * Unified dispatcher — picks the right `ColumnLazyImpl.fromX` by source
246
+ * Unified dispatcher — picks the right `DataColumnImpl.fromX` by source
212
247
  * shape. For ambiguous inputs callers can still use the explicit factories
213
- * (also attached as properties: `ColumnLazy.fromId`, `.fromPlRef`,
248
+ * (also attached as properties: `DataColumn.fromId`, `.fromPlRef`,
214
249
  * `.fromAccessor`, `.fromColumn`).
215
250
  */
216
- function ColumnLazyDispatch(
217
- source: PObjectId | PlRef | LeafEntry<TreeNodeAccessor> | PColumn<ColumnLazyData> | ColumnLazy,
251
+ function DataColumnDispatch(
252
+ source: DataColumnSource,
218
253
  opts: { ctx?: GlobalCfgRenderCtx } = {},
219
- ): undefined | ColumnLazy {
220
- if (typeof source === "string") return ColumnLazyImpl.fromId(source, opts);
221
- if (source instanceof ColumnLazyImpl) return source;
222
- if ("accessor" in source) return ColumnLazyImpl.fromAccessor(source);
223
- if (isPlRef(source)) return ColumnLazyImpl.fromPlRef(source);
224
- if (isPColumn(source)) return ColumnLazyImpl.fromColumn(source);
225
- throw new Error("ColumnLazy: unknown source shape");
254
+ ): undefined | DataColumnRecipe<PObjectId> {
255
+ if (typeof source === "string") return DataColumnImpl.fromId(source, opts);
256
+ if (source instanceof DataColumnImpl) return source;
257
+ // `DataColumnRecipe` is an interface — `instanceof` above cannot remove it from
258
+ // the union, and only `DataColumnImpl` implements it with a bare id.
259
+ const rest = source as LeafEntry<TreeNodeAccessor> | PlRef | PColumn<ColumnData>;
260
+ if ("accessor" in rest) return DataColumnImpl.fromAccessor(rest);
261
+ if (isPlRef(rest)) return DataColumnImpl.fromPlRef(rest);
262
+ if (isPColumn(rest)) return DataColumnImpl.fromColumn(rest);
263
+ throw new Error("DataColumn: unknown source shape");
226
264
  }
227
265
 
228
266
  /**
229
- * Polymorphic counterpart to {@link ColumnLazyDispatch}: returns the
267
+ * Polymorphic counterpart to {@link DataColumnDispatch}: returns the
230
268
  * {@link ColumnResolutionStatus} for any factory input without constructing
231
269
  * the recipe. For already-materialised sources ({@link PColumn} value,
232
- * existing {@link ColumnLazy}) status is `present` by construction.
270
+ * existing {@link DataColumnRecipe}) status is `present` by construction.
233
271
  */
234
- function ColumnLazyGetStatus(
235
- source: PObjectId | PlRef | LeafEntry<TreeNodeAccessor> | PColumn<ColumnLazyData> | ColumnLazy,
272
+ function DataColumnGetStatus(
273
+ source: DataColumnSource,
236
274
  opts: { ctx?: GlobalCfgRenderCtx } = {},
237
275
  ): ColumnResolutionStatus {
238
- if (typeof source === "string") return ColumnLazyImpl.getStatusById(source, opts);
239
- if (source instanceof ColumnLazyImpl) return "present";
240
- if ("accessor" in source) return ColumnLazyImpl.getStatusByAccessor(source);
241
- if (isPlRef(source)) return ColumnLazyImpl.getStatusByPlRef(source, opts);
242
- if (isPColumn(source)) return "present";
243
- throw new Error("ColumnLazy.getStatus: unknown source shape");
276
+ if (typeof source === "string") return DataColumnImpl.getStatusById(source, opts);
277
+ if (source instanceof DataColumnImpl) return "present";
278
+ const rest = source as LeafEntry<TreeNodeAccessor> | PlRef | PColumn<ColumnData>;
279
+ if ("accessor" in rest) return DataColumnImpl.getStatusByAccessor(rest);
280
+ if (isPlRef(rest)) return DataColumnImpl.getStatusByPlRef(rest, opts);
281
+ if (isPColumn(rest)) return "present";
282
+ throw new Error("DataColumn.getStatus: unknown source shape");
244
283
  }
245
284
 
246
- export const ColumnLazy = Object.assign(ColumnLazyDispatch, {
247
- fromId: ColumnLazyImpl.fromId,
248
- fromPlRef: ColumnLazyImpl.fromPlRef,
249
- fromAccessor: ColumnLazyImpl.fromAccessor,
250
- fromColumn: ColumnLazyImpl.fromColumn,
251
- getStatus: ColumnLazyGetStatus,
252
- getStatusById: ColumnLazyImpl.getStatusById,
253
- getStatusByPlRef: ColumnLazyImpl.getStatusByPlRef,
254
- getStatusByAccessor: ColumnLazyImpl.getStatusByAccessor,
285
+ export const DataColumn = Object.assign(DataColumnDispatch, {
286
+ fromId: DataColumnImpl.fromId,
287
+ fromPlRef: DataColumnImpl.fromPlRef,
288
+ fromAccessor: DataColumnImpl.fromAccessor,
289
+ fromColumn: DataColumnImpl.fromColumn,
290
+ getStatus: DataColumnGetStatus,
291
+ getStatusById: DataColumnImpl.getStatusById,
292
+ getStatusByPlRef: DataColumnImpl.getStatusByPlRef,
293
+ getStatusByAccessor: DataColumnImpl.getStatusByAccessor,
255
294
  });
256
295
 
257
296
  /**
258
- * Type-guard narrowing to the leaf recipe. Prefer this over
259
- * `instanceof ColumnLazyImpl` — `ColumnLazy` (the value) is the dispatcher
260
- * with statics, not the class, so `instanceof` requires the impl symbol.
297
+ * Type-guard narrowing to a bare leaf — a {@link DataColumnRecipe} whose `id` is a
298
+ * plain {@link PObjectId}.
261
299
  */
262
- export function isColumnLazy(value: unknown): value is ColumnLazy {
263
- return value instanceof ColumnLazyImpl;
300
+ export function isDataColumn(value: unknown): value is DataColumnRecipe<PObjectId> {
301
+ return value instanceof DataColumnImpl;
264
302
  }
265
303
 
266
304
  const readSpecAccessor = memoizeByEntry(
@@ -4,7 +4,7 @@ import type { ColumnsSource } from "./column_providers";
4
4
  import { ColumnsCollection, isColumnsCollection } from "./columns_collection";
5
5
  import type { GlobalCfgRenderCtx } from "../render/internal";
6
6
  import { TreeNodeAccessor } from "../render";
7
- import { isColumnLazy } from "./column_lazy";
7
+ import { hasReachableData } from "./utils";
8
8
 
9
9
  const RT_JSON = "PColumnData/Json";
10
10
  const RT_JSON_PARTITIONED = "PColumnData/JsonPartitioned";
@@ -18,11 +18,11 @@ const RT_JSON_PARTITIONED = "PColumnData/JsonPartitioned";
18
18
  * column-access mechanism (filtered {@link ColumnsCollection}) instead of
19
19
  * walking the raw result pool.
20
20
  *
21
- * Pair with {@link expandByPartition} (or any consumer expecting the
21
+ * Pair with {@link splitByAxes} (or any consumer expecting the
22
22
  * `(axisId) => Record<axisValue, label>` shape).
23
23
  *
24
24
  * Skips:
25
- * - non-leaf recipes (only direct `ColumnLazy` data is read);
25
+ * - recipes whose data is not directly readable (see `hasReachableData`);
26
26
  * - label columns whose `axesSpec.length !== 1`;
27
27
  * - label columns whose data resource type isn't `PColumnData/Json` /
28
28
  * `PColumnData/JsonPartitioned`.
@@ -43,7 +43,7 @@ export function deriveAxisValuesLabels(
43
43
  .getColumns();
44
44
 
45
45
  const byAxis = labelCols.reduce<Map<string, Record<string | number, string>>>((map, col) => {
46
- if (!isColumnLazy(col)) return map;
46
+ if (!hasReachableData(col)) return map;
47
47
  const spec = col.getSpec();
48
48
  if (spec.axesSpec.length !== 1) return map;
49
49
 
@@ -1,8 +1,8 @@
1
1
  export * from "./column_providers";
2
- export * from "./expand_by_partition";
2
+ export * from "./split_by_axes";
3
3
  export * from "./derive_axis_values_labels";
4
4
  export * from "./columns_collection";
5
- export * from "./column_lazy";
5
+ export * from "./data_column";
6
6
  export * from "./column_recipes";
7
7
  export * from "./column";
8
8
  export * from "./utils";
@@ -0,0 +1,124 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type {
3
+ AxisSpec,
4
+ PColumnSpec,
5
+ PObjectId,
6
+ SpecOverrides,
7
+ } from "@milaboratories/pl-model-common";
8
+ import type { PColumnDataUniversal } from "../render/internal";
9
+ import { DataColumnImpl, type DataColumnRecipe } from "./data_column";
10
+ import { ColumnFilteredRecipe } from "./column_recipes/column_filtered_recipe";
11
+ import { ColumnOverriddenRecipe } from "./column_recipes/column_overrided_recipe";
12
+ import { hasReachableData, hasSingleDataColumn } from "./utils";
13
+
14
+ // --- Helpers ---
15
+
16
+ const axis = (name: string): AxisSpec => ({ name, type: "String" }) as AxisSpec;
17
+
18
+ const twoAxisSpec: PColumnSpec = {
19
+ kind: "PColumn",
20
+ name: "count",
21
+ valueType: "Int",
22
+ axesSpec: [axis("sampleId"), axis("clonotypeKey")],
23
+ annotations: {},
24
+ } as PColumnSpec;
25
+
26
+ const overrides = (patch: Partial<SpecOverrides>): SpecOverrides => ({
27
+ annotations: patch.annotations ?? {},
28
+ domain: patch.domain ?? {},
29
+ contextDomain: patch.contextDomain ?? {},
30
+ axesSpec: patch.axesSpec ?? {},
31
+ });
32
+
33
+ const DATA = { marker: "leaf-data" } as unknown as PColumnDataUniversal;
34
+
35
+ const leaf = () =>
36
+ DataColumnImpl.fromColumn({
37
+ id: "leafId" as PObjectId,
38
+ spec: twoAxisSpec,
39
+ data: DATA,
40
+ });
41
+
42
+ // ════════════════════════════════════════════════════════════════════════════
43
+ // hasReachableData — "can I read this column's data, consistent with its spec?"
44
+ // ════════════════════════════════════════════════════════════════════════════
45
+
46
+ describe("hasReachableData", () => {
47
+ test("bare leaf: true, and getData returns the leaf payload", () => {
48
+ const col = leaf();
49
+ expect(hasReachableData(col)).toBe(true);
50
+ if (!hasReachableData(col)) throw new Error("unreachable");
51
+ expect(col.getData()).toBe(DATA);
52
+ });
53
+
54
+ test("override over a leaf: true — a spec patch never reshapes data", () => {
55
+ const col = ColumnOverriddenRecipe.wrap(leaf(), overrides({ annotations: { tag: "1" } }));
56
+ expect(hasReachableData(col)).toBe(true);
57
+ if (!hasReachableData(col)) throw new Error("unreachable");
58
+ expect(col.getData()).toBe(DATA);
59
+ // The patch landed on the spec while the data stayed the leaf's.
60
+ expect(col.getSpec().annotations?.tag).toBe("1");
61
+ expect(col.getSpec().axesSpec).toHaveLength(2);
62
+ });
63
+
64
+ test("axis-filtered leaf: false — spec drops an axis the data still carries", () => {
65
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
66
+ // The mismatch this guards against: one axis in the spec, two in the data.
67
+ expect(filtered.getSpec().axesSpec).toHaveLength(1);
68
+ expect(hasReachableData(filtered)).toBe(false);
69
+ });
70
+
71
+ test("override over an axis-filtered leaf: false", () => {
72
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
73
+ const col = ColumnOverriddenRecipe.wrap(filtered, overrides({ annotations: { tag: "1" } }));
74
+ expect(hasReachableData(col)).toBe(false);
75
+ });
76
+
77
+ test("an unreadable override has no getData at all — nothing to call, nothing to throw", () => {
78
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
79
+ const col = ColumnOverriddenRecipe.wrap(filtered, overrides({ annotations: { tag: "1" } }));
80
+ expect((col as Partial<DataColumnRecipe>).getData).toBeUndefined();
81
+ });
82
+
83
+ test("merging overrides keeps the readable variant", () => {
84
+ const once = leaf().withSpecs(overrides({ annotations: { a: "1" } }));
85
+ const twice = once.withSpecs(overrides({ annotations: { b: "2" } }));
86
+ expect(hasReachableData(twice)).toBe(true);
87
+ if (!hasReachableData(twice)) throw new Error("unreachable");
88
+ expect(twice.getData()).toBe(DATA);
89
+ expect(twice.getSpec().annotations).toMatchObject({ a: "1", b: "2" });
90
+ });
91
+
92
+ test("merging overrides does not resurrect readability over a filtered leaf", () => {
93
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
94
+ const twice = filtered
95
+ .withSpecs(overrides({ annotations: { a: "1" } }))
96
+ .withSpecs(overrides({ annotations: { b: "2" } }));
97
+ expect(hasReachableData(twice)).toBe(false);
98
+ });
99
+ });
100
+
101
+ // ════════════════════════════════════════════════════════════════════════════
102
+ // hasSingleDataColumn — "does this column read one data column, or several?"
103
+ // ════════════════════════════════════════════════════════════════════════════
104
+
105
+ describe("hasSingleDataColumn", () => {
106
+ test("bare leaf, override and axis filter all read a single data column", () => {
107
+ const bare = leaf();
108
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
109
+ const overridden = ColumnOverriddenRecipe.wrap(filtered, overrides({ domain: { d: "1" } }));
110
+
111
+ expect(hasSingleDataColumn(bare)).toBe(true);
112
+ expect(hasSingleDataColumn(filtered)).toBe(true);
113
+ expect(hasSingleDataColumn(overridden)).toBe(true);
114
+ });
115
+
116
+ test("is a different question from hasReachableData", () => {
117
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
118
+ // Reads a single data column, yet its data is not reachable here (the
119
+ // slice happens engine-side). Conflating the two is the bug this pair of
120
+ // predicates replaced.
121
+ expect(hasSingleDataColumn(filtered)).toBe(true);
122
+ expect(hasReachableData(filtered)).toBe(false);
123
+ });
124
+ });