@platforma-sdk/model 1.66.2 → 1.68.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.
- package/dist/block_model.d.ts +4 -0
- package/dist/block_model.d.ts.map +1 -1
- package/dist/block_state_util.cjs +2 -1
- package/dist/block_state_util.cjs.map +1 -1
- package/dist/block_state_util.js +2 -1
- package/dist/block_state_util.js.map +1 -1
- package/dist/columns/column_collection_builder.cjs +1 -1
- package/dist/columns/column_collection_builder.d.ts +1 -1
- package/dist/columns/column_collection_builder.js +1 -1
- package/dist/columns/index.cjs +2 -2
- package/dist/columns/index.d.ts +1 -1
- package/dist/columns/index.js +2 -2
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +1 -0
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +3 -3
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +1 -0
- package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +1 -1
- package/dist/components/PlDatasetSelector/build_dataset_options.cjs +41 -0
- package/dist/components/PlDatasetSelector/build_dataset_options.cjs.map +1 -0
- package/dist/components/PlDatasetSelector/build_dataset_options.d.ts +19 -0
- package/dist/components/PlDatasetSelector/build_dataset_options.d.ts.map +1 -0
- package/dist/components/PlDatasetSelector/build_dataset_options.js +41 -0
- package/dist/components/PlDatasetSelector/build_dataset_options.js.map +1 -0
- package/dist/components/PlDatasetSelector/filter_discovery.cjs +60 -0
- package/dist/components/PlDatasetSelector/filter_discovery.cjs.map +1 -0
- package/dist/components/PlDatasetSelector/filter_discovery.d.ts +31 -0
- package/dist/components/PlDatasetSelector/filter_discovery.d.ts.map +1 -0
- package/dist/components/PlDatasetSelector/filter_discovery.js +56 -0
- package/dist/components/PlDatasetSelector/filter_discovery.js.map +1 -0
- package/dist/components/PlDatasetSelector/index.cjs +2 -0
- package/dist/components/PlDatasetSelector/index.d.ts +2 -0
- package/dist/components/PlDatasetSelector/index.js +2 -0
- package/dist/components/index.cjs +3 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +3 -0
- package/dist/index.cjs +10 -4
- package/dist/index.d.ts +7 -5
- package/dist/index.js +7 -5
- package/dist/package.cjs +1 -1
- package/dist/package.js +1 -1
- package/dist/platforma.d.ts.map +1 -1
- package/dist/services/block_services.cjs +5 -1
- package/dist/services/block_services.cjs.map +1 -1
- package/dist/services/block_services.d.ts +2 -0
- package/dist/services/block_services.d.ts.map +1 -1
- package/dist/services/block_services.js +5 -1
- package/dist/services/block_services.js.map +1 -1
- package/package.json +7 -7
- package/src/block_state_util.ts +2 -1
- package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +1 -0
- package/src/components/PlDatasetSelector/build_dataset_options.ts +56 -0
- package/src/components/PlDatasetSelector/filter_discovery.test.ts +156 -0
- package/src/components/PlDatasetSelector/filter_discovery.ts +77 -0
- package/src/components/PlDatasetSelector/index.ts +2 -0
- package/src/components/index.ts +1 -0
- package/src/platforma.ts +2 -1
- package/src/services/block_services.ts +2 -0
- package/src/services/service_resolve.ts +0 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Annotation, createPlRef } from "@milaboratories/pl-model-common";
|
|
2
|
+
import type { AxisSpec, PColumnSpec, PlRef, PObjectId } from "@milaboratories/pl-model-common";
|
|
3
|
+
import { SpecDriver } from "@milaboratories/pf-spec-driver";
|
|
4
|
+
import canonicalize from "canonicalize";
|
|
5
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
6
|
+
import type { ColumnSnapshot } from "../../columns/column_snapshot";
|
|
7
|
+
import { ColumnCollectionBuilder } from "../../columns/column_collection_builder";
|
|
8
|
+
import { buildRefMap, filterMatchesToOptions, findFilterColumns } from "./filter_discovery";
|
|
9
|
+
|
|
10
|
+
const drivers: SpecDriver[] = [];
|
|
11
|
+
|
|
12
|
+
function createSpecFrameCtx() {
|
|
13
|
+
const driver = new SpecDriver();
|
|
14
|
+
drivers.push(driver);
|
|
15
|
+
return driver;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
for (const driver of drivers) await driver.dispose();
|
|
20
|
+
drivers.length = 0;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function axis(name: string): AxisSpec {
|
|
24
|
+
return { name, type: "String" } as AxisSpec;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function spec(
|
|
28
|
+
name: string,
|
|
29
|
+
axesSpec: AxisSpec[],
|
|
30
|
+
annotations: Record<string, string> = {},
|
|
31
|
+
): PColumnSpec {
|
|
32
|
+
return { kind: "PColumn", name, valueType: "Int", axesSpec, annotations } as PColumnSpec;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function snap(id: string, s: PColumnSpec): ColumnSnapshot<PObjectId> {
|
|
36
|
+
return { id: id as PObjectId, spec: s, dataStatus: "ready", data: { get: () => ({}) as never } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// anchor defines the key space: [sample, gene]
|
|
40
|
+
const anchorAxes = [axis("sample"), axis("gene")];
|
|
41
|
+
const anchorSpec = spec("anchor", anchorAxes);
|
|
42
|
+
const anchorSnap = snap("anchor-id", anchorSpec);
|
|
43
|
+
|
|
44
|
+
describe("findFilterColumns", () => {
|
|
45
|
+
test("returns columns with pl7.app/isSubset annotation", () => {
|
|
46
|
+
const filter = snap("f1", spec("filter1", [axis("sample")], { [Annotation.IsSubset]: "true" }));
|
|
47
|
+
const regular = snap("r1", spec("regular1", [axis("sample")]));
|
|
48
|
+
|
|
49
|
+
const builder = new ColumnCollectionBuilder(createSpecFrameCtx());
|
|
50
|
+
builder.addSource([filter, regular, anchorSnap]);
|
|
51
|
+
const collection = builder.build({ anchors: { main: anchorSpec } })!;
|
|
52
|
+
|
|
53
|
+
const results = findFilterColumns(collection);
|
|
54
|
+
expect(results.every((m) => m.column.spec.name !== "regular1")).toBe(true);
|
|
55
|
+
expect(results.some((m) => m.column.spec.name === "filter1")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("axes subset: excludes filter whose axes are not a subset of anchor axes", () => {
|
|
59
|
+
// filter with axis "other" — not a subset of anchor axes [sample, gene]
|
|
60
|
+
const badFilter = snap(
|
|
61
|
+
"f2",
|
|
62
|
+
spec("bad-filter", [axis("other")], { [Annotation.IsSubset]: "true" }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const builder = new ColumnCollectionBuilder(createSpecFrameCtx());
|
|
66
|
+
builder.addSource([badFilter, anchorSnap]);
|
|
67
|
+
const collection = builder.build({ anchors: { main: anchorSpec } })!;
|
|
68
|
+
|
|
69
|
+
const results = findFilterColumns(collection);
|
|
70
|
+
expect(results.every((m) => m.column.spec.name !== "bad-filter")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("empty result when no filters exist", () => {
|
|
74
|
+
const regular = snap("r1", spec("regular1", [axis("sample")]));
|
|
75
|
+
|
|
76
|
+
const builder = new ColumnCollectionBuilder(createSpecFrameCtx());
|
|
77
|
+
builder.addSource([regular, anchorSnap]);
|
|
78
|
+
const collection = builder.build({ anchors: { main: anchorSpec } })!;
|
|
79
|
+
|
|
80
|
+
expect(findFilterColumns(collection)).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("buildRefMap", () => {
|
|
85
|
+
test("maps canonicalized PlRef to original ref", () => {
|
|
86
|
+
const ref1 = createPlRef("b1", "out1");
|
|
87
|
+
const ref2 = createPlRef("b2", "out2", true);
|
|
88
|
+
const entries = [{ ref: ref1 }, { ref: ref2 }];
|
|
89
|
+
|
|
90
|
+
const map = buildRefMap(entries);
|
|
91
|
+
|
|
92
|
+
expect(map.get(canonicalize(ref1)! as PObjectId)).toBe(ref1);
|
|
93
|
+
expect(map.get(canonicalize(ref2)! as PObjectId)).toBe(ref2);
|
|
94
|
+
expect(map.size).toBe(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns empty map for empty entries", () => {
|
|
98
|
+
expect(buildRefMap([]).size).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("filterMatchesToOptions", () => {
|
|
103
|
+
test("converts filter matches to Option[] with derived labels", () => {
|
|
104
|
+
const filterRef1 = createPlRef("b1", "filter-top1000");
|
|
105
|
+
const filterRef2 = createPlRef("b1", "filter-highconf");
|
|
106
|
+
|
|
107
|
+
// Build ref map from entries (simulating result pool)
|
|
108
|
+
const refMap = buildRefMap([
|
|
109
|
+
{ ref: anchorSnap.id as unknown as PlRef }, // anchor — won't be looked up
|
|
110
|
+
{ ref: filterRef1 },
|
|
111
|
+
{ ref: filterRef2 },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
// Build filter specs with isSubset annotation
|
|
115
|
+
const filterSpec1 = spec("filter1", [axis("sample")], { [Annotation.IsSubset]: "true" });
|
|
116
|
+
const filterSpec2 = spec("filter2", [axis("sample")], { [Annotation.IsSubset]: "true" });
|
|
117
|
+
|
|
118
|
+
// Use the canonical PlRef as the PObjectId (matches how result pool works)
|
|
119
|
+
const f1Snap = snap(canonicalize(filterRef1)! as string, filterSpec1);
|
|
120
|
+
const f2Snap = snap(canonicalize(filterRef2)! as string, filterSpec2);
|
|
121
|
+
|
|
122
|
+
const builder = new ColumnCollectionBuilder(createSpecFrameCtx());
|
|
123
|
+
builder.addSource([f1Snap, f2Snap, anchorSnap]);
|
|
124
|
+
const collection = builder.build({ anchors: { main: anchorSpec } })!;
|
|
125
|
+
|
|
126
|
+
const matches = findFilterColumns(collection);
|
|
127
|
+
expect(matches.length).toBe(2);
|
|
128
|
+
|
|
129
|
+
const options = filterMatchesToOptions(matches, refMap);
|
|
130
|
+
expect(options).toHaveLength(2);
|
|
131
|
+
// Each option has a ref and label
|
|
132
|
+
for (const opt of options) {
|
|
133
|
+
expect(opt.ref).toBeDefined();
|
|
134
|
+
expect(opt.label).toBeDefined();
|
|
135
|
+
expect(typeof opt.label).toBe("string");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("returns empty array for empty matches", () => {
|
|
140
|
+
expect(filterMatchesToOptions([], new Map())).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("throws when ref not found in map", () => {
|
|
144
|
+
const filterSpec1 = spec("orphan", [axis("sample")], { [Annotation.IsSubset]: "true" });
|
|
145
|
+
const f1Snap = snap("orphan-id", filterSpec1);
|
|
146
|
+
|
|
147
|
+
const builder = new ColumnCollectionBuilder(createSpecFrameCtx());
|
|
148
|
+
builder.addSource([f1Snap, anchorSnap]);
|
|
149
|
+
const collection = builder.build({ anchors: { main: anchorSpec } })!;
|
|
150
|
+
|
|
151
|
+
const matches = findFilterColumns(collection);
|
|
152
|
+
expect(matches.length).toBe(1);
|
|
153
|
+
|
|
154
|
+
expect(() => filterMatchesToOptions(matches, new Map())).toThrow(/no PlRef found/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Annotation } from "@milaboratories/pl-model-common";
|
|
2
|
+
import type { Option, PlRef, PObjectId } from "@milaboratories/pl-model-common";
|
|
3
|
+
import canonicalize from "canonicalize";
|
|
4
|
+
import type {
|
|
5
|
+
AnchoredColumnCollection,
|
|
6
|
+
ColumnMatch,
|
|
7
|
+
} from "../../columns/column_collection_builder";
|
|
8
|
+
import {
|
|
9
|
+
deriveDistinctLabels,
|
|
10
|
+
type DeriveLabelsOptions,
|
|
11
|
+
type Entry,
|
|
12
|
+
} from "../../labels/derive_distinct_labels";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Matches columns annotated `pl7.app/isSubset: "true"` whose axes ⊆ anchor axes.
|
|
16
|
+
*
|
|
17
|
+
* The axes-subset constraint is enforced by `mode: "enrichment"`, which sets
|
|
18
|
+
* `allowFloatingHitAxes: false` — every axis of the matched column must be
|
|
19
|
+
* present in the anchor's axes. See `matchingModeToConstraints()` in
|
|
20
|
+
* `column_collection_builder.ts`.
|
|
21
|
+
*/
|
|
22
|
+
export function findFilterColumns(collection: AnchoredColumnCollection): ColumnMatch[] {
|
|
23
|
+
return collection.findColumns({
|
|
24
|
+
mode: "enrichment",
|
|
25
|
+
include: {
|
|
26
|
+
annotations: { [Annotation.IsSubset]: "true" },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Derive labeled options from filter column matches, for use in DatasetOption.filters.
|
|
33
|
+
*
|
|
34
|
+
* @param matches - from findFilterColumns()
|
|
35
|
+
* @param refsByObjectId - from {@link buildRefMap}
|
|
36
|
+
* @param labelOptions - forwarded to deriveDistinctLabels()
|
|
37
|
+
*/
|
|
38
|
+
export function filterMatchesToOptions(
|
|
39
|
+
matches: ColumnMatch[],
|
|
40
|
+
refsByObjectId: ReadonlyMap<PObjectId, PlRef>,
|
|
41
|
+
labelOptions?: DeriveLabelsOptions,
|
|
42
|
+
): Option[] {
|
|
43
|
+
if (matches.length === 0) return [];
|
|
44
|
+
|
|
45
|
+
// Each ColumnMatch can be reached via multiple variants (different linker
|
|
46
|
+
// paths / qualifications). We emit one Option per variant so the user can
|
|
47
|
+
// pick a specific path — `deriveDistinctLabels` disambiguates labels by
|
|
48
|
+
// path.
|
|
49
|
+
const flattened = matches.flatMap((m) => m.variants.map((v) => ({ match: m, variant: v })));
|
|
50
|
+
|
|
51
|
+
const entries: Entry[] = flattened.map(({ match, variant }) => ({
|
|
52
|
+
spec: match.column.spec,
|
|
53
|
+
linkerPath: variant.path.map((p) => ({ spec: p.linker.spec })),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const labels = deriveDistinctLabels(entries, labelOptions);
|
|
57
|
+
|
|
58
|
+
return flattened.map(({ match }, i) => {
|
|
59
|
+
const ref = refsByObjectId.get(match.column.id);
|
|
60
|
+
if (ref === undefined)
|
|
61
|
+
throw new Error(
|
|
62
|
+
`no PlRef found for filter column ${match.column.spec.name} (id: ${match.column.id})`,
|
|
63
|
+
);
|
|
64
|
+
return { ref, label: labels[i] };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Usage: `buildRefMap(ctx.resultPool.getSpecs().entries)`
|
|
70
|
+
*/
|
|
71
|
+
export function buildRefMap(entries: readonly { readonly ref: PlRef }[]): Map<PObjectId, PlRef> {
|
|
72
|
+
const map = new Map<PObjectId, PlRef>();
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
map.set(canonicalize(entry.ref)! as PObjectId, entry.ref);
|
|
75
|
+
}
|
|
76
|
+
return map;
|
|
77
|
+
}
|
package/src/components/index.ts
CHANGED
package/src/platforma.ts
CHANGED
|
@@ -7,9 +7,10 @@ import type {
|
|
|
7
7
|
BlockStateV3,
|
|
8
8
|
DriverKit,
|
|
9
9
|
OutputWithStatus,
|
|
10
|
+
UiServices as AllUiServices,
|
|
10
11
|
} from "@milaboratories/pl-model-common";
|
|
11
12
|
import type { SdkInfo } from "./version";
|
|
12
|
-
import type { ServiceDispatch
|
|
13
|
+
import type { ServiceDispatch } from "@milaboratories/pl-model-common";
|
|
13
14
|
import type { BlockStatePatch } from "./block_state_patch";
|
|
14
15
|
import type { PluginRecord } from "./block_model";
|
|
15
16
|
import type { PluginHandle, PluginFactoryLike } from "./plugin_handle";
|
|
@@ -10,6 +10,8 @@ import { resolveRequiredServices } from "@milaboratories/pl-model-common";
|
|
|
10
10
|
*/
|
|
11
11
|
export const BLOCK_SERVICE_FLAGS = {
|
|
12
12
|
requiresPFrameSpec: true,
|
|
13
|
+
requiresPFrame: true,
|
|
14
|
+
requiresDialog: true,
|
|
13
15
|
} as const satisfies Partial<ServiceRequireFlags>;
|
|
14
16
|
|
|
15
17
|
export type BlockServiceFlags = typeof BLOCK_SERVICE_FLAGS;
|
|
@@ -50,9 +50,7 @@ void _modelSpec;
|
|
|
50
50
|
void _uiSpec;
|
|
51
51
|
|
|
52
52
|
// Block default services do NOT include pframe (only plugins can request it)
|
|
53
|
-
// @ts-expect-error pframe is not in default block model services
|
|
54
53
|
const _modelPframe: BlockDefaultModelServices["pframe"] = undefined!;
|
|
55
|
-
// @ts-expect-error pframe is not in default block UI services
|
|
56
54
|
const _uiPframe: BlockDefaultUiServices["pframe"] = undefined!;
|
|
57
55
|
void _modelPframe;
|
|
58
56
|
void _uiPframe;
|