@milaboratories/pl-middle-layer 1.66.19 → 1.67.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_registry/index.cjs +3 -0
- package/dist/block_registry/index.d.ts +2 -1
- package/dist/block_registry/index.js +2 -1
- package/dist/block_registry/location_provider.cjs +117 -0
- package/dist/block_registry/location_provider.cjs.map +1 -0
- package/dist/block_registry/location_provider.js +114 -0
- package/dist/block_registry/location_provider.js.map +1 -0
- package/dist/block_registry/registry.cjs +13 -0
- package/dist/block_registry/registry.cjs.map +1 -1
- package/dist/block_registry/registry.d.ts +14 -1
- package/dist/block_registry/registry.d.ts.map +1 -1
- package/dist/block_registry/registry.js +13 -0
- package/dist/block_registry/registry.js.map +1 -1
- package/dist/block_registry/template_provider.cjs +104 -0
- package/dist/block_registry/template_provider.cjs.map +1 -0
- package/dist/block_registry/template_provider.d.ts +62 -0
- package/dist/block_registry/template_provider.d.ts.map +1 -0
- package/dist/block_registry/template_provider.js +103 -0
- package/dist/block_registry/template_provider.js.map +1 -0
- package/dist/block_registry/watcher.cjs +3 -3
- package/dist/block_registry/watcher.js +2 -2
- package/dist/dev_env/util.js +2 -2
- package/dist/dev_env/util.js.map +1 -1
- package/dist/index.cjs +10 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/js_render/computable_context.cjs +1 -1
- package/dist/js_render/computable_context.js +1 -1
- package/dist/middle_layer/driver_kit.d.ts +1 -1
- package/dist/middle_layer/middle_layer.cjs +144 -2
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +67 -1
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +146 -4
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project.cjs +1 -0
- package/dist/middle_layer/project.cjs.map +1 -1
- package/dist/middle_layer/project.d.ts +2 -2
- package/dist/middle_layer/project.js +4 -4
- package/dist/middle_layer/project.js.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +1 -1
- package/dist/model/args.cjs +9 -28
- package/dist/model/args.cjs.map +1 -1
- package/dist/model/args.js +9 -28
- package/dist/model/args.js.map +1 -1
- package/dist/model/index.cjs +7 -0
- package/dist/model/index.d.ts +4 -1
- package/dist/model/index.js +4 -1
- package/dist/model/project_helper.cjs +111 -0
- package/dist/model/project_helper.cjs.map +1 -1
- package/dist/model/project_helper.d.ts +76 -1
- package/dist/model/project_helper.d.ts.map +1 -1
- package/dist/model/project_helper.js +111 -0
- package/dist/model/project_helper.js.map +1 -1
- package/dist/model/project_model.d.ts +1 -1
- package/dist/model/sharing_model.d.ts +1 -1
- package/dist/model/template_apply.cjs +67 -0
- package/dist/model/template_apply.cjs.map +1 -0
- package/dist/model/template_apply.d.ts +50 -0
- package/dist/model/template_apply.d.ts.map +1 -0
- package/dist/model/template_apply.js +64 -0
- package/dist/model/template_apply.js.map +1 -0
- package/dist/model/template_export.cjs +82 -0
- package/dist/model/template_export.cjs.map +1 -0
- package/dist/model/template_export.d.ts +9 -0
- package/dist/model/template_export.d.ts.map +1 -0
- package/dist/model/template_export.js +82 -0
- package/dist/model/template_export.js.map +1 -0
- package/dist/model/template_parser.cjs +74 -0
- package/dist/model/template_parser.cjs.map +1 -0
- package/dist/model/template_parser.d.ts +43 -0
- package/dist/model/template_parser.d.ts.map +1 -0
- package/dist/model/template_parser.js +72 -0
- package/dist/model/template_parser.js.map +1 -0
- package/dist/model/template_resolve.cjs +134 -0
- package/dist/model/template_resolve.cjs.map +1 -0
- package/dist/model/template_resolve.d.ts +193 -0
- package/dist/model/template_resolve.d.ts.map +1 -0
- package/dist/model/template_resolve.js +133 -0
- package/dist/model/template_resolve.js.map +1 -0
- package/dist/model/template_serializer.cjs +162 -0
- package/dist/model/template_serializer.cjs.map +1 -0
- package/dist/model/template_serializer.d.ts +17 -0
- package/dist/model/template_serializer.d.ts.map +1 -0
- package/dist/model/template_serializer.js +157 -0
- package/dist/model/template_serializer.js.map +1 -0
- package/dist/mutator/block-pack/block_pack.cjs +1 -1
- package/dist/mutator/block-pack/block_pack.js +1 -1
- package/dist/mutator/migration.cjs +1 -1
- package/dist/mutator/migration.js +1 -1
- package/dist/mutator/project.cjs +35 -5
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +35 -5
- package/dist/mutator/project.js.map +1 -1
- package/dist/mutator/template_construct.cjs +77 -0
- package/dist/mutator/template_construct.cjs.map +1 -0
- package/dist/mutator/template_construct.js +77 -0
- package/dist/mutator/template_construct.js.map +1 -0
- package/dist/network_check/template.js +3 -3
- package/dist/network_check/template.js.map +1 -1
- package/package.json +18 -18
- package/src/block_registry/index.ts +1 -0
- package/src/block_registry/location_provider.test.ts +236 -0
- package/src/block_registry/location_provider.ts +124 -0
- package/src/block_registry/registry.ts +22 -0
- package/src/block_registry/template_provider.test.ts +275 -0
- package/src/block_registry/template_provider.ts +168 -0
- package/src/middle_layer/middle_layer.ts +192 -1
- package/src/middle_layer/project.ts +1 -1
- package/src/model/args.test.ts +26 -1
- package/src/model/args.ts +16 -31
- package/src/model/index.ts +21 -0
- package/src/model/project_helper.ts +238 -0
- package/src/model/project_helper_params_init.test.ts +249 -0
- package/src/model/template_apply.test.ts +65 -0
- package/src/model/template_apply.ts +128 -0
- package/src/model/template_export.test.ts +255 -0
- package/src/model/template_export.ts +136 -0
- package/src/model/template_parser.test.ts +264 -0
- package/src/model/template_parser.ts +108 -0
- package/src/model/template_resolve.test.ts +397 -0
- package/src/model/template_resolve.ts +346 -0
- package/src/model/template_serializer.test.ts +398 -0
- package/src/model/template_serializer.ts +232 -0
- package/src/model/template_serializer_fixtures.test.ts +274 -0
- package/src/mutator/project-v3.test.ts +11 -11
- package/src/mutator/project.ts +92 -5
- package/src/mutator/template_construct.test.ts +456 -0
- package/src/mutator/template_construct.ts +150 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import type { BlockKindReference, ColumnUniversalId } from "@milaboratories/pl-model-common";
|
|
6
|
+
import {
|
|
7
|
+
createColumnFilteredId,
|
|
8
|
+
createGlobalPObjectId,
|
|
9
|
+
createPlRef,
|
|
10
|
+
parseProjectTemplateV1,
|
|
11
|
+
} from "@milaboratories/pl-model-common";
|
|
12
|
+
import type { ProjectStructure } from "./project_model";
|
|
13
|
+
import type { TemplateParamsResult } from "./template_export";
|
|
14
|
+
import { exportProjectAsTemplateV1 } from "./template_serializer";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Golden-file tests for the exported document's TEXT.
|
|
18
|
+
*
|
|
19
|
+
* The sibling `template_serializer.test.ts` asserts the document field by field,
|
|
20
|
+
* which says nothing about how it is rendered. These pin the bytes: key order,
|
|
21
|
+
* indentation, how `params` nests, which scalars get quoted, and that nothing is
|
|
22
|
+
* line-folded. That is what "export emits exactly what import parses" rests on,
|
|
23
|
+
* and what makes a diff between two exported templates readable.
|
|
24
|
+
*
|
|
25
|
+
* Each expected file lives on disk rather than in a snapshot, deliberately. There
|
|
26
|
+
* is no snapshot infrastructure in this repo to fit into, and a snapshot carries an
|
|
27
|
+
* `-u` flag that rewrites the expectation without anyone reading it — for a file
|
|
28
|
+
* format promised to a second implementation, changing the expectation should be a
|
|
29
|
+
* deliberate edit that shows up in review. To update one, run the test, read the
|
|
30
|
+
* diff, and write the new content in by hand.
|
|
31
|
+
*
|
|
32
|
+
* Every fixture is also parsed back with the import-side parser, so a golden file
|
|
33
|
+
* can never be updated to something import cannot read.
|
|
34
|
+
*/
|
|
35
|
+
const FIXTURE_DIR = join(import.meta.dirname, "..", "..", "test_fixtures", "template-v1");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Params as the block's callback hands them over — live, and untouched on the way out. What a
|
|
39
|
+
* block projects is what the file holds, so there is nothing between these values and the
|
|
40
|
+
* rendered bytes.
|
|
41
|
+
*/
|
|
42
|
+
const ok = (value: unknown): TemplateParamsResult => ({ value });
|
|
43
|
+
|
|
44
|
+
const kind = (name: string, version: string) =>
|
|
45
|
+
`@platforma-open/milaboratories.${name}.kind@${version}` as BlockKindReference;
|
|
46
|
+
|
|
47
|
+
function structureOf(...ids: string[]): ProjectStructure {
|
|
48
|
+
return {
|
|
49
|
+
groups: [
|
|
50
|
+
{
|
|
51
|
+
id: "g1",
|
|
52
|
+
label: "Main",
|
|
53
|
+
blocks: ids.map((id) => ({ id, label: id, renderingMode: "Heavy" })),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Fixture = {
|
|
60
|
+
/** File under `test_fixtures/template-v1/`, and the test's name. */
|
|
61
|
+
readonly file: string;
|
|
62
|
+
/** What each fixture is here to pin — one property per fixture, not a grab bag. */
|
|
63
|
+
readonly pins: string;
|
|
64
|
+
readonly structure: ProjectStructure;
|
|
65
|
+
readonly params: Record<string, TemplateParamsResult>;
|
|
66
|
+
readonly kinds: Record<string, BlockKindReference>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const FIXTURES: readonly Fixture[] = [
|
|
70
|
+
{
|
|
71
|
+
file: "empty-project.yaml",
|
|
72
|
+
pins: "an empty project is still a valid document, not an empty file",
|
|
73
|
+
structure: structureOf(),
|
|
74
|
+
params: {},
|
|
75
|
+
kinds: {},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
file: "minimal.yaml",
|
|
79
|
+
pins: "the schema marker comes first, and a block with nothing to project renders `params: {}`",
|
|
80
|
+
structure: structureOf("11111111-1111-4111-8111-111111111111"),
|
|
81
|
+
params: { "11111111-1111-4111-8111-111111111111": ok({}) },
|
|
82
|
+
kinds: { "11111111-1111-4111-8111-111111111111": kind("pool-explorer", "1.0.0") },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
file: "linear-chain.yaml",
|
|
86
|
+
pins: "the canonical shape — three blocks wired in a chain, ids reused verbatim",
|
|
87
|
+
structure: structureOf(
|
|
88
|
+
"aaaaaaaa-0000-4000-8000-000000000001",
|
|
89
|
+
"bbbbbbbb-0000-4000-8000-000000000002",
|
|
90
|
+
"cccccccc-0000-4000-8000-000000000003",
|
|
91
|
+
),
|
|
92
|
+
params: {
|
|
93
|
+
"aaaaaaaa-0000-4000-8000-000000000001": ok({ dataset: "bulk-rna" }),
|
|
94
|
+
"bbbbbbbb-0000-4000-8000-000000000002": ok({
|
|
95
|
+
input: createPlRef("aaaaaaaa-0000-4000-8000-000000000001", "reads"),
|
|
96
|
+
species: "hsa",
|
|
97
|
+
}),
|
|
98
|
+
"cccccccc-0000-4000-8000-000000000003": ok({
|
|
99
|
+
clonotypes: createPlRef("bbbbbbbb-0000-4000-8000-000000000002", "clonotypes"),
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
kinds: {
|
|
103
|
+
"aaaaaaaa-0000-4000-8000-000000000001": kind("import-fastq", "2.1.0"),
|
|
104
|
+
"bbbbbbbb-0000-4000-8000-000000000002": kind("mixcr-clonotyping", "3.0.4"),
|
|
105
|
+
"cccccccc-0000-4000-8000-000000000003": kind("clonotype-browser", "1.2.10"),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
file: "nested-params.yaml",
|
|
110
|
+
pins: "how nesting renders — objects in arrays, references at depth, an empty object",
|
|
111
|
+
structure: structureOf(
|
|
112
|
+
"dddddddd-0000-4000-8000-000000000004",
|
|
113
|
+
"ffffffff-0000-4000-8000-000000000006",
|
|
114
|
+
),
|
|
115
|
+
params: {
|
|
116
|
+
"dddddddd-0000-4000-8000-000000000004": ok({}),
|
|
117
|
+
"ffffffff-0000-4000-8000-000000000006": ok({
|
|
118
|
+
thresholds: { min: 0.01, max: 1, exact: 0.5 },
|
|
119
|
+
steps: [
|
|
120
|
+
{ name: "filter", enabled: true },
|
|
121
|
+
{ name: "cluster", enabled: false, seed: 42 },
|
|
122
|
+
],
|
|
123
|
+
// Two references to the same upstream, inside an array.
|
|
124
|
+
inputs: [
|
|
125
|
+
createPlRef("dddddddd-0000-4000-8000-000000000004", "a"),
|
|
126
|
+
createPlRef("dddddddd-0000-4000-8000-000000000004", "b"),
|
|
127
|
+
],
|
|
128
|
+
advanced: {},
|
|
129
|
+
tags: [],
|
|
130
|
+
note: null,
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
kinds: {
|
|
134
|
+
"dddddddd-0000-4000-8000-000000000004": kind("import-fastq", "2.1.0"),
|
|
135
|
+
"ffffffff-0000-4000-8000-000000000006": kind("clonotype-clustering", "0.4.1"),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
file: "column-ids.yaml",
|
|
140
|
+
pins: "an identifier is written verbatim whatever its form — key object or canonical string",
|
|
141
|
+
structure: structureOf(
|
|
142
|
+
"aaaaaaaa-0000-4000-8000-000000000001",
|
|
143
|
+
"bbbbbbbb-0000-4000-8000-000000000002",
|
|
144
|
+
),
|
|
145
|
+
params: {
|
|
146
|
+
"aaaaaaaa-0000-4000-8000-000000000001": ok({}),
|
|
147
|
+
"bbbbbbbb-0000-4000-8000-000000000002": ok({
|
|
148
|
+
// A filtered column id — a canonical JSON string with the block id buried inside it.
|
|
149
|
+
// It goes into the file as it came: repointing it is the receiving block's business,
|
|
150
|
+
// in its own bundle, and nothing on the way out parses it.
|
|
151
|
+
anchor: createColumnFilteredId({
|
|
152
|
+
source: createGlobalPObjectId(
|
|
153
|
+
"aaaaaaaa-0000-4000-8000-000000000001",
|
|
154
|
+
"clonotypes",
|
|
155
|
+
) as ColumnUniversalId,
|
|
156
|
+
axisFilters: [[0, "IGH"]],
|
|
157
|
+
}),
|
|
158
|
+
// The same column in its object spelling. Both survive as written — no marker, no
|
|
159
|
+
// normalization, so a round trip cannot change which of the two a block gets back.
|
|
160
|
+
upstream: createPlRef("aaaaaaaa-0000-4000-8000-000000000001", "clonotypes"),
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
kinds: {
|
|
164
|
+
"aaaaaaaa-0000-4000-8000-000000000001": kind("import-fastq", "2.1.0"),
|
|
165
|
+
"bbbbbbbb-0000-4000-8000-000000000002": kind("clonotype-browser", "1.2.10"),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
file: "scalar-quoting.yaml",
|
|
170
|
+
pins: "strings that YAML would otherwise read back as something else stay strings",
|
|
171
|
+
structure: structureOf("eeeeeeee-0000-4000-8000-000000000005"),
|
|
172
|
+
params: {
|
|
173
|
+
"eeeeeeee-0000-4000-8000-000000000005": ok({
|
|
174
|
+
// Each of these is a real hazard: unquoted, some YAML reader turns them
|
|
175
|
+
// into a boolean, a number, a null, an alias, or a nested mapping.
|
|
176
|
+
// The first group is 1.1-only — a 1.2 emitter leaves them bare.
|
|
177
|
+
looksBoolean: "yes",
|
|
178
|
+
looksBooleanNo: "no",
|
|
179
|
+
looksBooleanOn: "on",
|
|
180
|
+
looksBooleanOff: "off",
|
|
181
|
+
looksBooleanY: "y",
|
|
182
|
+
looksBooleanN: "n",
|
|
183
|
+
looksSexagesimal: "1:30",
|
|
184
|
+
looksBooleanToo: "true",
|
|
185
|
+
looksNumeric: "1.0",
|
|
186
|
+
looksOctal: "0755",
|
|
187
|
+
looksNull: "null",
|
|
188
|
+
looksEmpty: "",
|
|
189
|
+
hasColon: "label: value",
|
|
190
|
+
startsWithAsterisk: "*anchor",
|
|
191
|
+
startsWithHash: "#not-a-comment",
|
|
192
|
+
startsWithDash: "-dash",
|
|
193
|
+
multiline: "first\nsecond",
|
|
194
|
+
unicode: "受容体 — αβ",
|
|
195
|
+
// Long enough to be folded at any default width; folding is switched off.
|
|
196
|
+
long: `${"x".repeat(200)} ${"y".repeat(200)}`,
|
|
197
|
+
actuallyBoolean: true,
|
|
198
|
+
actuallyNumeric: 1,
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
kinds: { "eeeeeeee-0000-4000-8000-000000000005": kind("enter-numbers", "1.0.0") },
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
function exportFixture(fixture: Fixture) {
|
|
206
|
+
const result = exportProjectAsTemplateV1(
|
|
207
|
+
fixture.structure,
|
|
208
|
+
(blockId) => fixture.params[blockId],
|
|
209
|
+
(blockId) => fixture.kinds[blockId],
|
|
210
|
+
// The golden files describe portable templates, so every block is a registry one:
|
|
211
|
+
// a locator would pin them to whichever machine produced the fixture.
|
|
212
|
+
() => ({
|
|
213
|
+
type: "from-registry-v2",
|
|
214
|
+
registryUrl: "https://block.registry.platforma.bio/releases",
|
|
215
|
+
id: { organization: "milaboratories", name: "demo", version: "1.0.0" },
|
|
216
|
+
channel: "stable",
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
if (!result.ok)
|
|
220
|
+
throw new Error(
|
|
221
|
+
`fixture '${fixture.file}' failed to export: ${result.problems.map((p) => p.error).join("; ")}`,
|
|
222
|
+
);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
describe.each(FIXTURES)("$file", (fixture) => {
|
|
227
|
+
test(`renders as the golden file — ${fixture.pins}`, () => {
|
|
228
|
+
const expected = readFileSync(join(FIXTURE_DIR, fixture.file), "utf-8");
|
|
229
|
+
|
|
230
|
+
// Compared as text, not as a parsed value: the parsed comparison is the test
|
|
231
|
+
// below, and it cannot see formatting at all.
|
|
232
|
+
expect(exportFixture(fixture).yaml).toBe(expected);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("the golden file parses back to the document it was rendered from", () => {
|
|
236
|
+
const result = exportFixture(fixture);
|
|
237
|
+
const fromDisk = readFileSync(join(FIXTURE_DIR, fixture.file), "utf-8");
|
|
238
|
+
|
|
239
|
+
expect(parseProjectTemplateV1(YAML.parse(fromDisk))).toEqual(result.document);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("what the golden files guard", () => {
|
|
244
|
+
test("no scalar is line-folded, however long", () => {
|
|
245
|
+
const yaml = exportFixture(FIXTURES.find((f) => f.file === "scalar-quoting.yaml")!).yaml;
|
|
246
|
+
|
|
247
|
+
// A folded scalar shows up as a continuation line; the value must stay on one.
|
|
248
|
+
const long = `${"x".repeat(200)} ${"y".repeat(200)}`;
|
|
249
|
+
expect(yaml).toContain(long);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("scalars only YAML 1.1 would misread are quoted", () => {
|
|
253
|
+
// The emitter quotes against 1.1 rules even though we parse as 1.2, because the
|
|
254
|
+
// file is read by other implementations: PyYAML and Go's yaml.v2 default to 1.1,
|
|
255
|
+
// where a bare `yes` is `true` and a bare `1:30` is `90`. A 1.2 emitter leaves
|
|
256
|
+
// both bare, so this is the one formatting choice that is an interop bug rather
|
|
257
|
+
// than a matter of taste — and a round trip through our own parser cannot catch
|
|
258
|
+
// it, since 1.2 reads them back as strings either way.
|
|
259
|
+
const yaml = exportFixture(FIXTURES.find((f) => f.file === "scalar-quoting.yaml")!).yaml;
|
|
260
|
+
|
|
261
|
+
for (const bare of ["yes", "no", "on", "off", "y", "n", "1:30"]) {
|
|
262
|
+
expect(yaml).toContain(`: "${bare}"`);
|
|
263
|
+
expect(yaml).not.toContain(`: ${bare}\n`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("a document with no blocks is an empty sequence, not a null", () => {
|
|
268
|
+
// `blocks:` alone would parse as null and fail the schema, turning an empty
|
|
269
|
+
// project into an unreadable file.
|
|
270
|
+
const yaml = exportFixture(FIXTURES.find((f) => f.file === "empty-project.yaml")!).yaml;
|
|
271
|
+
|
|
272
|
+
expect(yaml).toContain("blocks: []");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -14,13 +14,13 @@ import path from "node:path";
|
|
|
14
14
|
// These blocks use the new unified state format (state instead of args+uiState)
|
|
15
15
|
const BPSpecEnterV3: BlockPackSpec = {
|
|
16
16
|
type: "dev-v2",
|
|
17
|
-
// Navigate from lib/node/pl-middle-layer/src/mutator to etc/blocks/enter-numbers
|
|
18
|
-
folder: path.resolve(__dirname, "../../../../../etc/blocks/enter-numbers
|
|
17
|
+
// Navigate from lib/node/pl-middle-layer/src/mutator to etc/blocks/enter-numbers/block
|
|
18
|
+
folder: path.resolve(__dirname, "../../../../../etc/blocks/enter-numbers/block"),
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
const BPSpecSumV3: BlockPackSpec = {
|
|
22
22
|
type: "dev-v2",
|
|
23
|
-
folder: path.resolve(__dirname, "../../../../../etc/blocks/sum-numbers
|
|
23
|
+
folder: path.resolve(__dirname, "../../../../../etc/blocks/sum-numbers/block"),
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
test("v3 blocks: basic test with unified state", async () => {
|
|
@@ -34,7 +34,7 @@ test("v3 blocks: basic test with unified state", async () => {
|
|
|
34
34
|
return await toGlobalResourceId(prjRef);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
// Add enter-numbers
|
|
37
|
+
// Add enter-numbers block with storageMode: 'fromModel'
|
|
38
38
|
// Initial storage comes from VM, then we set desired state via setStates
|
|
39
39
|
await pl.withWriteTx("AddEnterNumbersV3Block", async (tx) => {
|
|
40
40
|
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
@@ -68,7 +68,7 @@ test("v3 blocks: basic test with unified state", async () => {
|
|
|
68
68
|
expect(JSON.parse(stateData).__data).toStrictEqual({ numbers: [1, 2, 3] });
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
// Add second enter-numbers
|
|
71
|
+
// Add second enter-numbers block
|
|
72
72
|
await pl.withWriteTx("AddEnterNumbersV3Block2", async (tx) => {
|
|
73
73
|
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
74
74
|
mut.addBlock(
|
|
@@ -93,7 +93,7 @@ test("v3 blocks: basic test with unified state", async () => {
|
|
|
93
93
|
await tx.commit();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
// Add sum-numbers
|
|
96
|
+
// Add sum-numbers block that references both enter blocks
|
|
97
97
|
await pl.withWriteTx("AddSumNumbersV3Block", async (tx) => {
|
|
98
98
|
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
99
99
|
mut.addBlock(
|
|
@@ -126,7 +126,7 @@ test("v3 blocks: basic test with unified state", async () => {
|
|
|
126
126
|
.get(projectFieldName("enter1", "stagingOutput"))
|
|
127
127
|
.then((r) => r.final());
|
|
128
128
|
const all = await stagingOutput.getAllFinal();
|
|
129
|
-
// V3 enter-numbers
|
|
129
|
+
// V3 enter-numbers block has prerun with numbersCount output
|
|
130
130
|
expect(Object.keys(all)).toContain("numbersCount");
|
|
131
131
|
});
|
|
132
132
|
|
|
@@ -167,7 +167,7 @@ test("v3 blocks: prerunArgs skip test", async () => {
|
|
|
167
167
|
return await toGlobalResourceId(prjRef);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
// Add enter-numbers
|
|
170
|
+
// Add enter-numbers block
|
|
171
171
|
await pl.withWriteTx("AddEnterNumbersV3Block", async (tx) => {
|
|
172
172
|
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
173
173
|
mut.addBlock(
|
|
@@ -296,7 +296,7 @@ test("v3 blocks: migrateBlockPack preserves state and re-derives args and prerun
|
|
|
296
296
|
return await toGlobalResourceId(prjRef);
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
-
// Add enter-numbers
|
|
299
|
+
// Add enter-numbers block and set data with even numbers
|
|
300
300
|
await pl.withWriteTx("AddBlock", async (tx) => {
|
|
301
301
|
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
302
302
|
mut.addBlock(
|
|
@@ -368,7 +368,7 @@ test("v3 blocks: migrateBlockPack with storage migration re-derives args and pre
|
|
|
368
368
|
return await toGlobalResourceId(prjRef);
|
|
369
369
|
});
|
|
370
370
|
|
|
371
|
-
// Add enter-numbers
|
|
371
|
+
// Add enter-numbers block with initial data
|
|
372
372
|
await pl.withWriteTx("AddBlock", async (tx) => {
|
|
373
373
|
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
374
374
|
mut.addBlock(
|
|
@@ -559,7 +559,7 @@ test("v3 blocks: migrateBlockPack assigns author marker", async () => {
|
|
|
559
559
|
// return undefined`) must not crash. Previously applyStorageAndDeriveArgs passed the
|
|
560
560
|
// undefined value straight to createJsonFieldValue -> Buffer.from(undefined) -> ERR_INVALID_ARG_TYPE,
|
|
561
561
|
// aborting the update-block-pack task (reported by AstraZeneca on mixcr-clonotyping 2.14 -> 2.20).
|
|
562
|
-
// The sentinel input [777] makes the enter-numbers
|
|
562
|
+
// The sentinel input [777] makes the enter-numbers model's args() return undefined without throwing.
|
|
563
563
|
test("v3 blocks: migrateBlockPack does not crash when args() returns undefined", async () => {
|
|
564
564
|
const quickJs = await getQuickJS();
|
|
565
565
|
|
package/src/mutator/project.ts
CHANGED
|
@@ -82,7 +82,11 @@ import {
|
|
|
82
82
|
UiError,
|
|
83
83
|
BLOCK_STORAGE_FACADE_VERSION,
|
|
84
84
|
type BlockConfig,
|
|
85
|
+
type BlockKindReference,
|
|
85
86
|
} from "@platforma-sdk/model";
|
|
87
|
+
import type { ProjectTemplateExportOutcome } from "../model/template_serializer";
|
|
88
|
+
import { exportProjectAsTemplateV1 } from "../model/template_serializer";
|
|
89
|
+
import type { TemplateParamsResult } from "../model/template_export";
|
|
86
90
|
import { getDebugFlags } from "../debug";
|
|
87
91
|
import type { BlockPackInfo } from "../model/block_pack";
|
|
88
92
|
|
|
@@ -112,6 +116,12 @@ interface BlockInfoState {
|
|
|
112
116
|
readonly id: string;
|
|
113
117
|
readonly fields: BlockFieldStates;
|
|
114
118
|
blockConfig?: BlockConfig;
|
|
119
|
+
/**
|
|
120
|
+
* The block's kind reference, kept separately because `blockConfig` cannot carry
|
|
121
|
+
* it: `extractConfig` normalizes the render envelope and the kind sits at the
|
|
122
|
+
* container level, one level above. Undefined for a block that declares no kind.
|
|
123
|
+
*/
|
|
124
|
+
blockKind?: BlockKindReference;
|
|
115
125
|
blockPack?: BlockPackSpec;
|
|
116
126
|
}
|
|
117
127
|
|
|
@@ -141,6 +151,8 @@ class BlockInfo {
|
|
|
141
151
|
public readonly fields: BlockFieldStates,
|
|
142
152
|
public readonly config: BlockConfig,
|
|
143
153
|
public readonly source: BlockPackSpec,
|
|
154
|
+
/** See {@link BlockInfoState.blockKind}. */
|
|
155
|
+
public readonly kind: BlockKindReference | undefined,
|
|
144
156
|
private readonly logger: MiLogger = new ConsoleLoggerAdapter(),
|
|
145
157
|
) {}
|
|
146
158
|
|
|
@@ -303,7 +315,25 @@ class BlockInfo {
|
|
|
303
315
|
*/
|
|
304
316
|
/** Specification for creating a new block. Discriminated union based on `storageMode`. */
|
|
305
317
|
export type NewBlockSpec =
|
|
306
|
-
| {
|
|
318
|
+
| {
|
|
319
|
+
storageMode: "fromModel";
|
|
320
|
+
blockPack: BlockPackSpecPrepared;
|
|
321
|
+
/**
|
|
322
|
+
* Storage to start the block with, instead of the model's own default.
|
|
323
|
+
*
|
|
324
|
+
* Only for a caller that produced it through the block's own model, which today
|
|
325
|
+
* means one seeding a block from a template entry's params: the model's
|
|
326
|
+
* params-to-storage initializer can reject those params, and it has to do so
|
|
327
|
+
* before anything in the transaction changes. Computing the storage first turns
|
|
328
|
+
* that rejection into a value the caller can report, and leaves this method with
|
|
329
|
+
* no failure path it did not already have.
|
|
330
|
+
*
|
|
331
|
+
* The result is the same storage the block would have written itself, so args
|
|
332
|
+
* derivation below is unchanged — which is why this is the `fromModel` mode with
|
|
333
|
+
* one input filled in rather than a mode of its own.
|
|
334
|
+
*/
|
|
335
|
+
initialStorage?: string;
|
|
336
|
+
}
|
|
307
337
|
| { storageMode: "legacy"; blockPack: BlockPackSpecPrepared; legacyState: string };
|
|
308
338
|
|
|
309
339
|
const NoNewBlocks = (blockId: string) => {
|
|
@@ -516,6 +546,48 @@ export class ProjectMutator {
|
|
|
516
546
|
};
|
|
517
547
|
}
|
|
518
548
|
|
|
549
|
+
//
|
|
550
|
+
// Template export
|
|
551
|
+
//
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Render this project as a `template-v1` YAML document, or report every reason it
|
|
555
|
+
* cannot be.
|
|
556
|
+
*
|
|
557
|
+
* Read-only: it derives params in the VM and touches no field, so a `withProject`
|
|
558
|
+
* wrapper sees `wasModified === false` and skips the commit.
|
|
559
|
+
*
|
|
560
|
+
* This is where the three providers the serializer needs come from, and all are
|
|
561
|
+
* only reachable here. A block's storage lives behind `BlockInfo`, which is
|
|
562
|
+
* populated by the loader's batched round-trips, and both its kind reference and
|
|
563
|
+
* its origin spec are read off the block-pack container during that same load —
|
|
564
|
+
* `config` cannot carry the kind, since `extractConfig` normalizes the render
|
|
565
|
+
* envelope one level below it, and the spec is not part of the config at all.
|
|
566
|
+
*/
|
|
567
|
+
public exportAsTemplateV1(): ProjectTemplateExportOutcome {
|
|
568
|
+
return exportProjectAsTemplateV1(
|
|
569
|
+
this.struct,
|
|
570
|
+
(blockId) => this.deriveTemplateParams(blockId),
|
|
571
|
+
(blockId) => this.blockInfos.get(blockId)?.kind,
|
|
572
|
+
(blockId) => this.blockInfos.get(blockId)?.source,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private deriveTemplateParams(blockId: string): TemplateParamsResult | undefined {
|
|
577
|
+
const info = this.blockInfos.get(blockId);
|
|
578
|
+
// A block in the structure with no loaded info, or with no storage yet, has no
|
|
579
|
+
// state to project into params. Returning undefined makes the walk report it
|
|
580
|
+
// rather than quietly leaving the block out of the template.
|
|
581
|
+
if (info === undefined) return undefined;
|
|
582
|
+
|
|
583
|
+
const storageJson = info.blockStorageJson;
|
|
584
|
+
if (storageJson === undefined) return undefined;
|
|
585
|
+
|
|
586
|
+
const derived = this.projectHelper.deriveTemplateParamsFromStorage(info.config, storageJson);
|
|
587
|
+
if (derived.error !== undefined) return { error: derived.error.message };
|
|
588
|
+
return { value: derived.value };
|
|
589
|
+
}
|
|
590
|
+
|
|
519
591
|
private getPendingProductionGraph(): BlockGraph {
|
|
520
592
|
if (this.pendingProductionGraph === undefined)
|
|
521
593
|
this.pendingProductionGraph = productionGraph(this.struct, (blockId) =>
|
|
@@ -1142,6 +1214,7 @@ export class ProjectMutator {
|
|
|
1142
1214
|
{},
|
|
1143
1215
|
extractConfig(spec.blockPack.config),
|
|
1144
1216
|
spec.blockPack.source,
|
|
1217
|
+
spec.blockPack.config.kind,
|
|
1145
1218
|
this.projectHelper.logger,
|
|
1146
1219
|
);
|
|
1147
1220
|
this.blockInfos.set(blockId, info);
|
|
@@ -1164,8 +1237,11 @@ export class ProjectMutator {
|
|
|
1164
1237
|
let storageToWrite: string;
|
|
1165
1238
|
|
|
1166
1239
|
if (spec.storageMode === "fromModel") {
|
|
1167
|
-
// Model API v2+: get initial storage and derive args from it
|
|
1168
|
-
|
|
1240
|
+
// Model API v2+: get initial storage and derive args from it. A caller that
|
|
1241
|
+
// already produced the storage — a template seeding a block from its params —
|
|
1242
|
+
// passes it in, precisely so the VM call that could reject those params happens
|
|
1243
|
+
// before the transaction is touched; see `initialStorage`.
|
|
1244
|
+
storageToWrite = spec.initialStorage ?? this.projectHelper.getInitialStorageInVM(blockConfig);
|
|
1169
1245
|
|
|
1170
1246
|
// Derive prerunArgs first — always derived independently of args validation
|
|
1171
1247
|
prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(blockConfig, storageToWrite);
|
|
@@ -1251,6 +1327,7 @@ export class ProjectMutator {
|
|
|
1251
1327
|
{},
|
|
1252
1328
|
originalBlockInfo.config,
|
|
1253
1329
|
originalBlockInfo.source,
|
|
1330
|
+
originalBlockInfo.kind,
|
|
1254
1331
|
this.projectHelper.logger,
|
|
1255
1332
|
);
|
|
1256
1333
|
|
|
@@ -1895,6 +1972,9 @@ export class ProjectMutator {
|
|
|
1895
1972
|
const result = await response;
|
|
1896
1973
|
const bpInfo = cachedDeserialize<BlockPackInfo>(notEmpty(result.data));
|
|
1897
1974
|
info.blockConfig = extractConfig(bpInfo.config);
|
|
1975
|
+
// Read off the container before `extractConfig`'s result replaces it as the
|
|
1976
|
+
// only thing we keep. Costs no round-trip — `bpInfo` is already in hand.
|
|
1977
|
+
info.blockKind = bpInfo.config.kind;
|
|
1898
1978
|
info.blockPack = bpInfo.source;
|
|
1899
1979
|
}
|
|
1900
1980
|
|
|
@@ -1926,10 +2006,17 @@ export class ProjectMutator {
|
|
|
1926
2006
|
const blocksInLimboSet = new Set(blocksInLimbo);
|
|
1927
2007
|
|
|
1928
2008
|
const blockInfos = new Map<string, BlockInfo>();
|
|
1929
|
-
blockInfoStates.forEach(({ id, fields, blockConfig, blockPack }) =>
|
|
2009
|
+
blockInfoStates.forEach(({ id, fields, blockConfig, blockKind, blockPack }) =>
|
|
1930
2010
|
blockInfos.set(
|
|
1931
2011
|
id,
|
|
1932
|
-
new BlockInfo(
|
|
2012
|
+
new BlockInfo(
|
|
2013
|
+
id,
|
|
2014
|
+
fields,
|
|
2015
|
+
notEmpty(blockConfig),
|
|
2016
|
+
notEmpty(blockPack),
|
|
2017
|
+
blockKind,
|
|
2018
|
+
projectHelper.logger,
|
|
2019
|
+
),
|
|
1933
2020
|
),
|
|
1934
2021
|
);
|
|
1935
2022
|
|