@platforma-sdk/model 1.69.0 → 1.71.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 (83) hide show
  1. package/dist/columns/column_collection_builder.cjs +1 -4
  2. package/dist/columns/column_collection_builder.cjs.map +1 -1
  3. package/dist/columns/column_collection_builder.d.ts +0 -2
  4. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  5. package/dist/columns/column_collection_builder.js +1 -4
  6. package/dist/columns/column_collection_builder.js.map +1 -1
  7. package/dist/columns/column_snapshot_provider.cjs +26 -2
  8. package/dist/columns/column_snapshot_provider.cjs.map +1 -1
  9. package/dist/columns/column_snapshot_provider.d.ts +2 -1
  10. package/dist/columns/column_snapshot_provider.d.ts.map +1 -1
  11. package/dist/columns/column_snapshot_provider.js +25 -2
  12. package/dist/columns/column_snapshot_provider.js.map +1 -1
  13. package/dist/columns/ctx_column_sources.cjs +5 -8
  14. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  15. package/dist/columns/ctx_column_sources.d.ts +4 -7
  16. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  17. package/dist/columns/ctx_column_sources.js +5 -8
  18. package/dist/columns/ctx_column_sources.js.map +1 -1
  19. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +41 -24
  20. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  21. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  22. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +42 -25
  23. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  24. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +1 -2
  25. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  26. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +1 -2
  27. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  28. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +16 -4
  29. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
  30. package/dist/components/PlDataTable/createPlDataTable/utils.js +16 -5
  31. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
  32. package/dist/components/PlDatasetSelector/build_dataset_options.cjs +23 -11
  33. package/dist/components/PlDatasetSelector/build_dataset_options.cjs.map +1 -1
  34. package/dist/components/PlDatasetSelector/build_dataset_options.d.ts +9 -2
  35. package/dist/components/PlDatasetSelector/build_dataset_options.d.ts.map +1 -1
  36. package/dist/components/PlDatasetSelector/build_dataset_options.js +22 -11
  37. package/dist/components/PlDatasetSelector/build_dataset_options.js.map +1 -1
  38. package/dist/components/PlDatasetSelector/dataset_selection.cjs +20 -0
  39. package/dist/components/PlDatasetSelector/dataset_selection.cjs.map +1 -0
  40. package/dist/components/PlDatasetSelector/dataset_selection.d.ts +23 -0
  41. package/dist/components/PlDatasetSelector/dataset_selection.d.ts.map +1 -0
  42. package/dist/components/PlDatasetSelector/dataset_selection.js +19 -0
  43. package/dist/components/PlDatasetSelector/dataset_selection.js.map +1 -0
  44. package/dist/components/PlDatasetSelector/enrichment_discovery.cjs +75 -0
  45. package/dist/components/PlDatasetSelector/enrichment_discovery.cjs.map +1 -0
  46. package/dist/components/PlDatasetSelector/enrichment_discovery.js +73 -0
  47. package/dist/components/PlDatasetSelector/enrichment_discovery.js.map +1 -0
  48. package/dist/components/PlDatasetSelector/index.cjs +1 -0
  49. package/dist/components/PlDatasetSelector/index.d.ts +1 -0
  50. package/dist/components/PlDatasetSelector/index.js +1 -0
  51. package/dist/components/index.cjs +1 -0
  52. package/dist/components/index.d.ts +1 -0
  53. package/dist/components/index.js +1 -0
  54. package/dist/index.cjs +3 -0
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.js +2 -1
  57. package/dist/labels/derive_distinct_tooltips.cjs +0 -3
  58. package/dist/labels/derive_distinct_tooltips.cjs.map +1 -1
  59. package/dist/labels/derive_distinct_tooltips.js +0 -3
  60. package/dist/labels/derive_distinct_tooltips.js.map +1 -1
  61. package/dist/package.cjs +1 -1
  62. package/dist/package.js +1 -1
  63. package/dist/render/api.cjs +3 -14
  64. package/dist/render/api.cjs.map +1 -1
  65. package/dist/render/api.d.ts.map +1 -1
  66. package/dist/render/api.js +4 -15
  67. package/dist/render/api.js.map +1 -1
  68. package/dist/render/index.cjs +1 -1
  69. package/dist/render/index.js +1 -1
  70. package/package.json +9 -9
  71. package/src/columns/column_collection_builder.ts +0 -3
  72. package/src/columns/column_snapshot_provider.ts +30 -14
  73. package/src/columns/ctx_column_sources.ts +6 -12
  74. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +58 -44
  75. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +0 -1
  76. package/src/components/PlDataTable/createPlDataTable/utils.ts +25 -3
  77. package/src/components/PlDatasetSelector/build_dataset_options.ts +48 -17
  78. package/src/components/PlDatasetSelector/dataset_selection.ts +37 -0
  79. package/src/components/PlDatasetSelector/enrichment_discovery.ts +111 -0
  80. package/src/components/PlDatasetSelector/index.ts +1 -0
  81. package/src/labels/derive_distinct_tooltips.test.ts +6 -16
  82. package/src/labels/derive_distinct_tooltips.ts +0 -3
  83. package/src/render/api.ts +5 -17
@@ -1,11 +1,9 @@
1
1
  import type { PObjectId } from "@milaboratories/pl-model-common";
2
- import { PColumn } from "@milaboratories/pl-model-common";
2
+ import { isDataInfo, PColumn, visitDataInfo } from "@milaboratories/pl-model-common";
3
3
  import { TreeNodeAccessor } from "../render/accessor";
4
4
  import type { PColumnDataUniversal } from "../render/internal";
5
5
  import type { ColumnDataStatus, ColumnSnapshot } from "./column_snapshot";
6
6
 
7
- // --- ColumnProvider ---
8
-
9
7
  /**
10
8
  * Data source interface for column enumeration.
11
9
  *
@@ -22,8 +20,6 @@ export interface ColumnSnapshotProvider {
22
20
  isColumnListComplete(): boolean;
23
21
  }
24
22
 
25
- // --- ColumnSource ---
26
-
27
23
  /**
28
24
  * Union of types that can serve as column sources for helpers and builders.
29
25
  * Does NOT include TreeNodeAccessor — call `.toColumnSource()` on it first.
@@ -33,8 +29,6 @@ export type ColumnSource =
33
29
  | ColumnSnapshot<PObjectId>[]
34
30
  | PColumn<PColumnDataUniversal | undefined>[];
35
31
 
36
- // --- ArrayColumnProvider ---
37
-
38
32
  /**
39
33
  * Simple provider wrapping an array of PColumns.
40
34
  * Always complete, data status always 'ready'.
@@ -46,8 +40,8 @@ export class ArrayColumnProvider implements ColumnSnapshotProvider {
46
40
  this.columns = columns.map((col) => ({
47
41
  id: col.id,
48
42
  spec: col.spec,
49
- dataStatus: "ready" as const,
50
43
  data: { get: () => col.data },
44
+ dataStatus: this.getStatus(col.data),
51
45
  }));
52
46
  }
53
47
 
@@ -58,9 +52,35 @@ export class ArrayColumnProvider implements ColumnSnapshotProvider {
58
52
  isColumnListComplete(): boolean {
59
53
  return true;
60
54
  }
61
- }
62
55
 
63
- // --- SnapshotColumnProvider ---
56
+ protected getStatus(
57
+ d: undefined | PColumnDataUniversal | (() => undefined | PColumnDataUniversal),
58
+ ): ColumnDataStatus {
59
+ if (d == null) {
60
+ return "absent";
61
+ }
62
+ if (typeof d === "function") {
63
+ return this.getStatus(d());
64
+ }
65
+ if (d instanceof TreeNodeAccessor) {
66
+ if (d.getIsReadyOrError()) return "ready";
67
+ if (d.getIsFinal()) return "absent";
68
+ return "computing";
69
+ }
70
+ if (isDataInfo(d)) {
71
+ let ready = true;
72
+ let final = true;
73
+ visitDataInfo(d, (v) => {
74
+ ready &&= v.getIsReadyOrError();
75
+ final &&= v.getIsFinal();
76
+ });
77
+ if (ready) return "ready";
78
+ if (final) return "absent";
79
+ return "computing";
80
+ }
81
+ return "ready";
82
+ }
83
+ }
64
84
 
65
85
  /**
66
86
  * Provider wrapping an array of ColumnSnapshots.
@@ -78,8 +98,6 @@ export class SnapshotColumnProvider implements ColumnSnapshotProvider {
78
98
  }
79
99
  }
80
100
 
81
- // --- OutputColumnProvider ---
82
-
83
101
  export interface OutputColumnProviderOpts {
84
102
  /** When true and the accessor is final, columns with no ready data get status 'absent'. */
85
103
  allowPermanentAbsence?: boolean;
@@ -133,8 +151,6 @@ export class OutputColumnProvider implements ColumnSnapshotProvider {
133
151
  }
134
152
  }
135
153
 
136
- // --- Source normalization ---
137
-
138
154
  /** Checks if a value is a ColumnSnapshotProvider (duck-typing). */
139
155
  export function isColumnSnapshotProvider(source: unknown): source is ColumnSnapshotProvider {
140
156
  return (
@@ -9,32 +9,26 @@ import { ResourceTypeName } from "@milaboratories/pl-model-common";
9
9
  import type { ValueOf } from "@milaboratories/helpers";
10
10
 
11
11
  /**
12
- * Collect ColumnSnapshotProviders from all render context sources:
13
- *
14
- * - **resultPool** all upstream columns (always included)
15
- * - **outputs** PFrame fields from block execution outputs
16
- * - **prerun** — PFrame fields from prerun/staging results
17
- *
18
- * Returns an array of providers suitable for `ColumnCollectionBuilder.addSource()`.
12
+ * Collect ColumnSnapshotProviders from `outputs`, `prerun`, and
13
+ * `resultPool` in that order. Dedup keeps the first occurrence per
14
+ * `NativePObjectId`, so a block re-publishing its own columns keeps
15
+ * the `outputs`-rooted canonical id instead of the result-pool variant.
19
16
  */
20
17
  export function collectCtxColumnSnapshotProviders(ctx: RenderCtxBase): ColumnSnapshotProvider[] {
21
18
  const providers: ColumnSnapshotProvider[] = [];
22
19
 
23
- // ResultPool — all upstream columns
24
- providers.push(new ResultPoolColumnSnapshotProvider(ctx.resultPool));
25
-
26
- // Outputs — each PFrame-like output field becomes a provider
27
20
  const outputs = ctx.outputs;
28
21
  if (outputs) {
29
22
  providers.push(...collectPFrameProviders(outputs));
30
23
  }
31
24
 
32
- // Prerun — same treatment as outputs
33
25
  const prerun = ctx.prerun;
34
26
  if (prerun) {
35
27
  providers.push(...collectPFrameProviders(prerun));
36
28
  }
37
29
 
30
+ providers.push(new ResultPoolColumnSnapshotProvider(ctx.resultPool));
31
+
38
32
  return providers;
39
33
  }
40
34
 
@@ -32,6 +32,7 @@ import {
32
32
  withLabelAnnotations,
33
33
  withTableVisualAnnotations,
34
34
  withInfoAnnotations,
35
+ withDataStatusAnnotations,
35
36
  } from "./utils";
36
37
  import type { PrimaryEntry, SecondaryGroup } from "./createPTableDefV3";
37
38
  import { createPTableDefV3 } from "./createPTableDefV3";
@@ -79,8 +80,6 @@ export type ColumnVisibilityRule = {
79
80
 
80
81
  export type ColumnMatcher = (spec: PColumnSpec) => boolean;
81
82
 
82
- // Main Function
83
-
84
83
  export function createPlDataTableV3<A, U>(
85
84
  ctx: RenderCtxBase<A, U>,
86
85
  options: createPlDataTableOptionsV3,
@@ -138,17 +137,18 @@ export function createPlDataTableV3<A, U>(
138
137
  ]);
139
138
 
140
139
  const remapedDefaultFilters = remapFilterColumnIds(options.filters, discovered);
141
- const filters = concatFilters(
142
- state.pTableParams.filters,
143
- state.pTableParams.defaultFilters ?? remapedDefaultFilters,
140
+ const filters = filterFilters(
141
+ concatFilters(
142
+ state.pTableParams.filters,
143
+ state.pTableParams.defaultFilters ?? remapedDefaultFilters,
144
+ ),
145
+ columnIsAvailable,
144
146
  );
145
- validateFilters(filters, columnIsAvailable);
146
147
 
147
- const sorting = resolveSorting(
148
- state.pTableParams.sorting,
149
- remapSortingColumnIds(options.sorting, discovered),
148
+ const sorting = filterSorting(
149
+ resolveSorting(state.pTableParams.sorting, remapSortingColumnIds(options.sorting, discovered)),
150
+ columnIsAvailable,
150
151
  );
151
- validateSorting(sorting, columnIsAvailable);
152
152
 
153
153
  const primaryEntries: PrimaryEntry<undefined | PColumnDataUniversal>[] = primarySnapshots.map(
154
154
  (v) => ({ column: resolveSnapshot(v.column) }),
@@ -274,6 +274,7 @@ function annotateColumnGroups(params: {
274
274
  const directAnnotated = liftToVariantColumns(
275
275
  direct,
276
276
  flow(
277
+ (cols) => withDataStatusAnnotations(cols),
277
278
  (cols) => withLabelAnnotations(derivedLabels, cols),
278
279
  (cols) => withInfoAnnotations(derivedTooltips, cols),
279
280
  (cols) => withTableVisualAnnotations(visibilityByColId, orderByColId, cols),
@@ -283,6 +284,7 @@ function annotateColumnGroups(params: {
283
284
  const linkedAnnotated = liftToVariantColumns(
284
285
  linked,
285
286
  flow(
287
+ (cols) => withDataStatusAnnotations(cols),
286
288
  (cols) => withHidenAxesAnnotations(cols),
287
289
  (cols) => withLabelAnnotations(derivedLabels, cols),
288
290
  (cols) => withInfoAnnotations(derivedTooltips, cols),
@@ -344,19 +346,35 @@ function createColumnValidationById(
344
346
  };
345
347
  }
346
348
 
347
- /** Validate that all column references in filters exist in the table. */
348
- function validateFilters(
349
+ /** Drop filter leaves whose column references are not available in the table. */
350
+ function filterFilters(
349
351
  filters: Nil | PlDataTableFilters,
350
352
  isValidColumnId: (id: string) => boolean,
351
- ): void {
352
- if (filters == null) return;
353
- const filterColumns = collectFilterSpecColumns(filters);
354
- const firstInvalid = filterColumns.find((col) => !isValidColumnId(col));
355
- if (firstInvalid !== undefined) {
356
- throw new Error(
357
- `Invalid filter column ${firstInvalid}: column reference does not match the table columns`,
358
- );
359
- }
353
+ ): Nil | PlDataTableFilters {
354
+ if (isNil(filters)) return filters;
355
+
356
+ const isLeafValid = (leaf: PlDataTableFilterSpecLeaf): boolean => {
357
+ if (leaf.type === undefined) return true;
358
+ if ("column" in leaf && !isValidColumnId(leaf.column)) return false;
359
+ if ("rhs" in leaf && !isValidColumnId(leaf.rhs)) return false;
360
+ return true;
361
+ };
362
+
363
+ const prune = (node: PlDataTableFilterNode): Nil | PlDataTableFilterNode => {
364
+ if (node.type === "and" || node.type === "or") {
365
+ const kept = node.filters
366
+ .map((f) => prune(f))
367
+ .filter((f): f is PlDataTableFilterNode => !isNil(f));
368
+ return { type: node.type, filters: kept };
369
+ }
370
+ if (node.type === "not") {
371
+ const inner = prune(node.filter);
372
+ return isNil(inner) ? undefined : { type: "not", filter: inner };
373
+ }
374
+ return isLeafValid(node) ? node : undefined;
375
+ };
376
+
377
+ return prune(filters) as Nil | PlDataTableFilters;
360
378
  }
361
379
 
362
380
  /** Merge two filter trees into one AND-combined tree. Returns the non-nil one if the other is nil. */
@@ -377,16 +395,12 @@ function resolveSorting(
377
395
  return (isEmpty(userSorting) ? defaultSorting : userSorting) ?? [];
378
396
  }
379
397
 
380
- /** Validate that all column references in sorting exist in the table. */
381
- function validateSorting(sorting: PTableSorting[], isValidColumnId: (id: string) => boolean): void {
382
- const firstInvalid = sorting.find(
383
- (s) => !isValidColumnId(canonicalizeJson<PTableColumnId>(s.column)),
384
- );
385
- if (firstInvalid !== undefined) {
386
- throw new Error(
387
- `Invalid sorting column ${JSON.stringify(firstInvalid.column)}: column reference does not match the table columns`,
388
- );
389
- }
398
+ /** Drop sorting entries whose column is not available in the table. */
399
+ function filterSorting(
400
+ sorting: PTableSorting[],
401
+ isValidColumnId: (id: string) => boolean,
402
+ ): PTableSorting[] {
403
+ return sorting.filter((s) => isValidColumnId(canonicalizeJson<PTableColumnId>(s.column)));
390
404
  }
391
405
 
392
406
  function buildSecondaryGroups(
@@ -405,7 +419,6 @@ function buildSecondaryGroups(
405
419
  entries: [
406
420
  ...lc.path.map((s) => ({
407
421
  column: resolveSnapshot(s.linker),
408
- qualifications: s.qualifications,
409
422
  })),
410
423
  { column: resolveSnapshot(lc.column), qualifications: lc.qualifications.forHit },
411
424
  ],
@@ -474,21 +487,22 @@ function remapSortingColumnIds(
474
487
  sorting: Nil | PTableSorting[],
475
488
  columns: TableColumnVariant[],
476
489
  ): Nil | PTableSorting[] {
477
- return sorting?.map((s) => {
478
- if (s.column.type === "axis") return s; // Axis references are unaffected by column ID remapping
490
+ return sorting?.flatMap((s) => {
491
+ if (s.column.type === "axis") return [s]; // Axis references are unaffected by column ID remapping
479
492
 
480
493
  const id = s.column.id;
481
- const column =
482
- columns.find((c) => (c.originalId ?? c.column.id) === id) ??
483
- throwError(`Column ID "${id}" in sorting does not match any discovered column`);
484
-
485
- return {
486
- ...s,
487
- column: {
488
- type: "column",
489
- id: column.column.id,
494
+ const column = columns.find((c) => (c.originalId ?? c.column.id) === id);
495
+ if (column === undefined) return [];
496
+
497
+ return [
498
+ {
499
+ ...s,
500
+ column: {
501
+ type: "column" as const,
502
+ id: column.column.id,
503
+ },
490
504
  },
491
- };
505
+ ];
492
506
  });
493
507
  }
494
508
 
@@ -102,7 +102,6 @@ function mapToTableColumnVariants(
102
102
  path: variant.path.map((p) => ({
103
103
  type: "linker",
104
104
  column: p.linker.id,
105
- qualifications: p.qualifications,
106
105
  })),
107
106
  columnQualifications: variant.qualifications.forHit,
108
107
  queriesQualifications: variant.qualifications.forQueries,
@@ -17,7 +17,7 @@ import {
17
17
  deriveDistinctTooltips,
18
18
  type TooltipEntry,
19
19
  } from "../../../labels/derive_distinct_tooltips";
20
- import type { MatchQualifications, MatchVariant } from "../../../columns";
20
+ import type { ColumnDataStatus, MatchQualifications, MatchVariant } from "../../../columns";
21
21
  import type { ColumnMatcher, ColumnOrderRule, ColumnVisibilityRule } from "./createPlDataTableV3";
22
22
  import type { ColumnSelector } from "../../../columns";
23
23
  import { ArrayColumnProvider, ColumnCollectionBuilder } from "../../../columns";
@@ -130,7 +130,10 @@ function dedupeById(columns: RuleColumn[]): RuleColumn[] {
130
130
  * For each axis in column specs: writes derived axis label into AxisSpec annotations.
131
131
  */
132
132
  export function withLabelAnnotations<
133
- T extends { readonly id: PObjectId; readonly spec: PColumnSpec },
133
+ T extends {
134
+ readonly id: PObjectId;
135
+ readonly spec: PColumnSpec;
136
+ },
134
137
  >(derivedLabels: undefined | Record<string, string>, columns: T[]): T[] {
135
138
  if (derivedLabels === undefined) return columns;
136
139
  return columns.map((col) => {
@@ -153,6 +156,26 @@ export function withLabelAnnotations<
153
156
  });
154
157
  }
155
158
 
159
+ export function withDataStatusAnnotations<
160
+ T extends {
161
+ readonly spec: PColumnSpec;
162
+ readonly dataStatus: ColumnDataStatus;
163
+ },
164
+ >(columns: T[]): T[] {
165
+ return columns.map((col) => {
166
+ return {
167
+ ...col,
168
+ spec: {
169
+ ...col.spec,
170
+ annotations: {
171
+ ...col.spec.annotations,
172
+ [Annotation.DataStatus]: col.dataStatus,
173
+ },
174
+ },
175
+ } as T;
176
+ });
177
+ }
178
+
156
179
  /**
157
180
  * Writes effective display properties (OrderPriority, Visibility) from precomputed rule maps
158
181
  * into column annotations. Returns new column objects — originals are not mutated.
@@ -241,7 +264,6 @@ export function deriveAllLabels(options: {
241
264
  spec: c.spec,
242
265
  linkerPath: c.linkerPath?.map((step) => ({
243
266
  spec: step.linker.spec,
244
- qualifications: step.qualifications,
245
267
  })),
246
268
  qualifications: c.qualifications,
247
269
  }));
@@ -1,20 +1,25 @@
1
- import type {
2
- DatasetOption,
3
- Option,
4
- PColumnSelector,
5
- PObjectSpec,
6
- } from "@milaboratories/pl-model-common";
1
+ import type { MultiColumnSelector, Option, PObjectSpec } from "@milaboratories/pl-model-common";
2
+ import { multiColumnSelectorsToPredicate } from "@milaboratories/pl-model-common";
7
3
  import type { DeriveLabelsOptions } from "../../labels/derive_distinct_labels";
8
4
  import type { RenderCtxBase } from "../../render";
9
5
  import { ColumnCollectionBuilder } from "../../columns/column_collection_builder";
10
6
  import { collectCtxColumnSnapshotProviders } from "../../columns/ctx_column_sources";
7
+ import type { DatasetOption } from "./dataset_selection";
11
8
  import { buildRefMap, filterMatchesToOptions, findFilterColumns } from "./filter_discovery";
9
+ import { enrichmentVariantsToRefs, findEnrichmentColumns } from "./enrichment_discovery";
12
10
 
13
11
  export type BuildDatasetOptions = {
14
12
  /** Which result pool columns qualify as datasets. Defaults to all. */
15
- selector?: PColumnSelector | PColumnSelector[] | ((spec: PObjectSpec) => boolean);
13
+ primary?: MultiColumnSelector | MultiColumnSelector[] | ((spec: PObjectSpec) => boolean);
16
14
  /** Formatting options for filter labels. */
17
15
  labelOptions?: DeriveLabelsOptions;
16
+ /**
17
+ * Enables enrichment discovery and filters hits attached to
18
+ * `DatasetOption.enrichments`. Use `() => true` to accept all; omit to disable.
19
+ */
20
+ withEnrichments?: MultiColumnSelector | MultiColumnSelector[] | ((spec: PObjectSpec) => boolean);
21
+ /** Maximum linker hops considered. Only used when `withEnrichments` is set. */
22
+ enrichmentMaxHops?: number;
18
23
  };
19
24
 
20
25
  /**
@@ -27,28 +32,54 @@ export function buildDatasetOptions(
27
32
  ctx: RenderCtxBase,
28
33
  opts?: BuildDatasetOptions,
29
34
  ): DatasetOption[] | undefined {
30
- const predicate = opts?.selector ?? (() => true);
31
- const options = ctx.resultPool.getOptions(predicate, { refsWithEnrichments: true });
35
+ const primary = opts?.primary;
36
+ const primaryPredicate =
37
+ primary === undefined
38
+ ? () => true
39
+ : typeof primary === "function"
40
+ ? primary
41
+ : multiColumnSelectorsToPredicate(primary);
42
+ const options = ctx.resultPool.getOptions(primaryPredicate, { refsWithEnrichments: true });
32
43
  if (options.length === 0) return [];
33
44
 
34
45
  const columnSources = collectCtxColumnSnapshotProviders(ctx);
35
46
  const refMap = buildRefMap(ctx.resultPool.getSpecs().entries);
36
47
  const pframeSpec = ctx.getService("pframeSpec");
37
48
 
38
- return options.map((o: Option): DatasetOption => {
39
- const datasetSpec = ctx.resultPool.getPColumnSpecByRef(o.ref);
40
- if (!datasetSpec) return o;
49
+ return options.map((primary: Option): DatasetOption => {
50
+ const datasetSpec = ctx.resultPool.getPColumnSpecByRef(primary.ref);
51
+ if (!datasetSpec) return { primary };
41
52
 
42
53
  const builder = new ColumnCollectionBuilder(pframeSpec);
43
54
  for (const src of columnSources) builder.addSource(src);
44
55
  const collection = builder.build({ anchors: { main: datasetSpec } });
45
- if (!collection) return o;
56
+ if (!collection) return { primary };
46
57
 
47
58
  try {
48
- const matches = findFilterColumns(collection);
49
- if (matches.length === 0) return o;
50
- const filters = filterMatchesToOptions(matches, refMap, opts?.labelOptions);
51
- return { ...o, filters };
59
+ const filterMatches = findFilterColumns(collection);
60
+ const filters =
61
+ filterMatches.length === 0
62
+ ? undefined
63
+ : filterMatchesToOptions(filterMatches, refMap, opts?.labelOptions);
64
+
65
+ let enrichments;
66
+ if (opts?.withEnrichments !== undefined) {
67
+ const enrichmentVariants = findEnrichmentColumns(collection, {
68
+ maxHops: opts.enrichmentMaxHops,
69
+ ...(typeof opts.withEnrichments === "function"
70
+ ? { predicate: opts.withEnrichments }
71
+ : { include: opts.withEnrichments }),
72
+ });
73
+ if (enrichmentVariants.length > 0) {
74
+ enrichments = enrichmentVariantsToRefs(enrichmentVariants, opts.labelOptions);
75
+ }
76
+ }
77
+
78
+ return {
79
+ primary,
80
+ ...(filters !== undefined && filters.length > 0 ? { filters } : {}),
81
+ ...(enrichments !== undefined && enrichments.length > 0 ? { enrichments } : {}),
82
+ };
52
83
  } finally {
53
84
  collection.dispose();
54
85
  }
@@ -0,0 +1,37 @@
1
+ import type { LabeledEnrichmentRefs, Option, PrimaryRef } from "@milaboratories/pl-model-common";
2
+
3
+ /** Dataset picker entry: user picks {@link primary}, gets {@link enrichments} attached. */
4
+ export type DatasetOption = {
5
+ readonly primary: Option;
6
+ readonly filters?: readonly Option[];
7
+ readonly enrichments?: LabeledEnrichmentRefs;
8
+ };
9
+
10
+ /**
11
+ * Picked dataset bundle emitted by `PlDatasetSelector`. Stored opaquely in
12
+ * block data; block authors unbundle inside their args resolver.
13
+ */
14
+ export type DatasetSelection = {
15
+ readonly __isDatasetSelection: "v1";
16
+ readonly primary: PrimaryRef;
17
+ readonly enrichments?: LabeledEnrichmentRefs;
18
+ };
19
+
20
+ export function isDatasetSelection(value: unknown): value is DatasetSelection {
21
+ return (
22
+ typeof value === "object" &&
23
+ value !== null &&
24
+ (value as { __isDatasetSelection?: unknown }).__isDatasetSelection === "v1" &&
25
+ "primary" in value
26
+ );
27
+ }
28
+
29
+ export function createDatasetSelection(
30
+ primary: PrimaryRef,
31
+ enrichments?: LabeledEnrichmentRefs,
32
+ ): DatasetSelection {
33
+ if (enrichments !== undefined && enrichments.length > 0) {
34
+ return { __isDatasetSelection: "v1", primary, enrichments };
35
+ }
36
+ return { __isDatasetSelection: "v1", primary };
37
+ }
@@ -0,0 +1,111 @@
1
+ import { Annotation, createEnrichmentRef } from "@milaboratories/pl-model-common";
2
+ import type {
3
+ EnrichmentStep,
4
+ LabeledEnrichmentRef,
5
+ LabeledEnrichmentRefs,
6
+ MultiColumnSelector,
7
+ PObjectId,
8
+ PObjectSpec,
9
+ } from "@milaboratories/pl-model-common";
10
+ import type {
11
+ AnchoredColumnCollection,
12
+ ColumnVariant,
13
+ } from "../../columns/column_collection_builder";
14
+ import {
15
+ deriveDistinctLabels,
16
+ type DeriveLabelsOptions,
17
+ type Entry,
18
+ } from "../../labels/derive_distinct_labels";
19
+
20
+ /**
21
+ * True for global-form ids — `canonicalize({__isRef: true, blockId, name})` —
22
+ * which the workflow can resolve via bquery. Local-form ids (`resolvePath`)
23
+ * fail this check and are excluded from auto-discovery; prerun/outputs hops
24
+ * must be supplied as resolved `{spec, data}` instead.
25
+ */
26
+ function isGloballyAddressable(id: PObjectId): boolean {
27
+ try {
28
+ const decoded = JSON.parse(id);
29
+ return (
30
+ typeof decoded === "object" &&
31
+ decoded !== null &&
32
+ decoded.__isRef === true &&
33
+ typeof decoded.blockId === "string" &&
34
+ typeof decoded.name === "string"
35
+ );
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Linker-reached hits attached to the anchor primary. Drops zero-hop variants
43
+ * (filters / the primary itself) and structural hits (subset, linker, label
44
+ * columns). Narrow further with `include` selectors or a `predicate`.
45
+ */
46
+ export function findEnrichmentColumns(
47
+ collection: AnchoredColumnCollection,
48
+ options?: {
49
+ maxHops?: number;
50
+ include?: MultiColumnSelector | MultiColumnSelector[];
51
+ predicate?: (spec: PObjectSpec) => boolean;
52
+ },
53
+ ): ColumnVariant[] {
54
+ const include =
55
+ options?.include === undefined
56
+ ? undefined
57
+ : Array.isArray(options.include)
58
+ ? options.include
59
+ : [options.include];
60
+ const variants = collection.findColumnVariants({
61
+ mode: "enrichment",
62
+ maxHops: options?.maxHops ?? 4,
63
+ include,
64
+ exclude: [
65
+ { annotations: { [Annotation.IsSubset]: "true" } },
66
+ { annotations: { [Annotation.IsLinkerColumn]: "true" } },
67
+ { name: Annotation.Label },
68
+ ],
69
+ });
70
+ const predicate = options?.predicate;
71
+ return variants.filter((v) => {
72
+ if (v.path.length === 0) return false;
73
+ if (predicate !== undefined && !predicate(v.column.spec)) return false;
74
+ if (!isGloballyAddressable(v.column.id)) return false;
75
+ if (v.path.some((p) => !isGloballyAddressable(p.linker.id))) return false;
76
+ return true;
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Pair each variant with a path-disambiguated label (so export headers stay
82
+ * unique) and carry hit/linker `PObjectId`s through verbatim. Propagates
83
+ * `qualifications.forHit`; `forQueries` is re-derived by the table builder.
84
+ */
85
+ export function enrichmentVariantsToRefs(
86
+ variants: ColumnVariant[],
87
+ labelOptions?: DeriveLabelsOptions,
88
+ ): LabeledEnrichmentRefs {
89
+ if (variants.length === 0) return [];
90
+
91
+ const entries: Entry[] = variants.map((variant) => ({
92
+ spec: variant.column.spec,
93
+ linkerPath: variant.path.map((p) => ({ spec: p.linker.spec })),
94
+ qualifications: variant.qualifications,
95
+ }));
96
+ const labels = deriveDistinctLabels(entries, labelOptions);
97
+
98
+ return variants.map((variant, i): LabeledEnrichmentRef => {
99
+ const path: EnrichmentStep[] = variant.path.map((step) => ({
100
+ type: "linker",
101
+ linker: step.linker.id,
102
+ }));
103
+ return {
104
+ ref: createEnrichmentRef(variant.column.id, {
105
+ path,
106
+ qualifications: variant.qualifications.forHit,
107
+ }),
108
+ label: labels[i],
109
+ };
110
+ });
111
+ }
@@ -1,2 +1,3 @@
1
+ export * from "./dataset_selection";
1
2
  export * from "./filter_discovery";
2
3
  export * from "./build_dataset_options";