@milaboratories/pl-middle-layer 1.66.19 → 1.67.1
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 +17 -17
- 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
|
@@ -20,7 +20,23 @@ import {
|
|
|
20
20
|
ProjectsField,
|
|
21
21
|
ProjectsResourceType,
|
|
22
22
|
} from "./project_list";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
createProject,
|
|
25
|
+
duplicateProject,
|
|
26
|
+
withProject,
|
|
27
|
+
withProjectAuthored,
|
|
28
|
+
} from "../mutator/project";
|
|
29
|
+
import type { ProjectTemplateExportOutcome } from "../model/template_serializer";
|
|
30
|
+
import type { ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
31
|
+
import { extractConfig, ensureError } from "@platforma-sdk/model";
|
|
32
|
+
import type { TemplateApplyProblem, TemplateApplyReport } from "../model/template_apply";
|
|
33
|
+
import { TemplateEntryRejected, kindMismatch } from "../model/template_apply";
|
|
34
|
+
import type { BlockPackProvider } from "../model/template_resolve";
|
|
35
|
+
import { resolveTemplateEntries } from "../model/template_resolve";
|
|
36
|
+
import type { PreparedTemplateEntry } from "../mutator/template_construct";
|
|
37
|
+
import { applyTemplateEntries } from "../mutator/template_construct";
|
|
38
|
+
import { throwIfMissingServerCapabilities } from "./project";
|
|
39
|
+
import { cacheBlockPackTemplate } from "../mutator/template/template_cache";
|
|
24
40
|
import { ProjectMetaKey } from "../model/project_model";
|
|
25
41
|
import type { ProjectId } from "../model/project_model";
|
|
26
42
|
import type { SynchronizedTreeState } from "@milaboratories/pl-tree";
|
|
@@ -75,6 +91,7 @@ import type {
|
|
|
75
91
|
ProjectMeta,
|
|
76
92
|
BlockPlatform,
|
|
77
93
|
} from "@milaboratories/pl-model-middle-layer";
|
|
94
|
+
import type { AppliedEntry } from "../model/template_apply";
|
|
78
95
|
import { BlockUpdateWatcher } from "../block_registry/watcher";
|
|
79
96
|
import type { QuickJSWASMModule } from "quickjs-emscripten";
|
|
80
97
|
import { getQuickJS } from "quickjs-emscripten";
|
|
@@ -325,6 +342,180 @@ export class MiddleLayer {
|
|
|
325
342
|
await this.projectListTree.refreshState();
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Renders a project as a `template-v1` YAML document, or reports every reason it
|
|
347
|
+
* cannot be — the backing call for an "Export Project as Template…" command.
|
|
348
|
+
*
|
|
349
|
+
* Takes a project id rather than an open {@link Project} because exporting is a
|
|
350
|
+
* property of the stored project, not of a session with it: the command belongs on
|
|
351
|
+
* a project card, where the project is usually closed. Opening one to read it would
|
|
352
|
+
* spin up trees and watchers for a one-shot read, and then have to decide whether to
|
|
353
|
+
* close them again.
|
|
354
|
+
*
|
|
355
|
+
* Read-only — the underlying mutator touches no field, so the transaction is never
|
|
356
|
+
* committed and the project list needs no refresh.
|
|
357
|
+
*
|
|
358
|
+
* @param id - project id of the project to export
|
|
359
|
+
*/
|
|
360
|
+
public async exportProjectAsTemplate(id: ProjectId): Promise<ProjectTemplateExportOutcome> {
|
|
361
|
+
const rid = await this.resolveProjectId(id);
|
|
362
|
+
return await withProject(
|
|
363
|
+
this.env.projectHelper,
|
|
364
|
+
this.pl,
|
|
365
|
+
rid,
|
|
366
|
+
(prj) => prj.exportAsTemplateV1(),
|
|
367
|
+
{ name: "exportProjectAsTemplate" },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Creates the blocks a `template-v1` document describes in an existing project, in the
|
|
373
|
+
* order the document lists them — the backing call for a "Create Project from Template
|
|
374
|
+
* file…" command, which is `createProject` followed by this.
|
|
375
|
+
*
|
|
376
|
+
* Takes a project id rather than an open {@link Project}, like
|
|
377
|
+
* {@link exportProjectAsTemplate} and for the same reason: applying a template is a
|
|
378
|
+
* property of the stored project, not of a session with it, and the flow that needs it
|
|
379
|
+
* has just created the project and has no session yet. An already-open session picks the
|
|
380
|
+
* new blocks up through its own refresh loop.
|
|
381
|
+
*
|
|
382
|
+
* Three stages, and their order is the design:
|
|
383
|
+
*
|
|
384
|
+
* 1. **Resolve every entry** to a concrete block pack, through `provider`.
|
|
385
|
+
* 2. **Prepare every block**: fetch it, check it can run against this backend, cache its
|
|
386
|
+
* workflow template, and offer the entry's params to the block's kind for a shape
|
|
387
|
+
* check.
|
|
388
|
+
* 3. **Create the blocks**, in one transaction — each one's params first pointed at this
|
|
389
|
+
* project by the block's own model, since which values in there are references is
|
|
390
|
+
* knowledge only the block has.
|
|
391
|
+
*
|
|
392
|
+
* The first two create nothing, so either of them failing leaves the project exactly as it
|
|
393
|
+
* was. They are also what leaves stage 3 with only in-memory work, and hence able to be a
|
|
394
|
+
* single transaction.
|
|
395
|
+
*
|
|
396
|
+
* **Stage 3 is all or nothing too**, because it is that one transaction: an entry it cannot
|
|
397
|
+
* create throws, the transaction is never committed, and the project keeps none of the
|
|
398
|
+
* blocks the apply had placed. So `problems` non-empty always means `added` is empty, at
|
|
399
|
+
* every stage — a caller never has to reconcile a half-built project, and never has to ask
|
|
400
|
+
* which of the blocks present came from the file.
|
|
401
|
+
*
|
|
402
|
+
* What is NOT checked before the work starts: which entries an entry references. Reading
|
|
403
|
+
* that means reading the params, which only the block can do, and no block exists until
|
|
404
|
+
* stage 2 has fetched one. A file whose entry references one listed below it therefore
|
|
405
|
+
* applies, and the block it creates reports itself as missing references — the same way a
|
|
406
|
+
* reference to a deleted block already behaves.
|
|
407
|
+
*
|
|
408
|
+
* @param id Project to apply into
|
|
409
|
+
* @param document A parsed template document
|
|
410
|
+
* @param provider Where each entry's block comes from
|
|
411
|
+
* @param options `allowUnstable` widens resolution to pre-release implementations, for
|
|
412
|
+
* the whole document
|
|
413
|
+
*/
|
|
414
|
+
public async applyTemplateToProject(
|
|
415
|
+
id: ProjectId,
|
|
416
|
+
document: ProjectTemplateV1,
|
|
417
|
+
provider: BlockPackProvider,
|
|
418
|
+
options: { allowUnstable?: boolean; author?: AuthorMarker } = {},
|
|
419
|
+
): Promise<TemplateApplyReport> {
|
|
420
|
+
const resolution = await resolveTemplateEntries(document, provider, {
|
|
421
|
+
allowUnstable: options.allowUnstable ?? false,
|
|
422
|
+
});
|
|
423
|
+
if (resolution.problems.length > 0) return { added: [], problems: resolution.problems };
|
|
424
|
+
|
|
425
|
+
// One map, not one per field: resolution reports by entry id, so everything this loop
|
|
426
|
+
// needs about an entry is looked up the same way.
|
|
427
|
+
const byEntryId = new Map(document.blocks.map((entry) => [entry.id, entry]));
|
|
428
|
+
const prepared = new Map<string, PreparedTemplateEntry>();
|
|
429
|
+
const problems: TemplateApplyProblem[] = [];
|
|
430
|
+
|
|
431
|
+
for (const entry of resolution.resolved) {
|
|
432
|
+
try {
|
|
433
|
+
const documentEntry = byEntryId.get(entry.entryId)!;
|
|
434
|
+
const preparedBp = await this.env.bpPreparer.prepare(entry.spec);
|
|
435
|
+
const blockCfg = extractConfig(preparedBp.config);
|
|
436
|
+
|
|
437
|
+
// The first question asked of the prepared block: is it the block this entry meant.
|
|
438
|
+
// Everything below is only meaningful once the answer is yes.
|
|
439
|
+
//
|
|
440
|
+
// The locator is appended here rather than inside the check, which names no route:
|
|
441
|
+
// when the file chose the implementation itself, what it chose is the thing to correct.
|
|
442
|
+
const mismatch = kindMismatch(documentEntry.kind, preparedBp.config.kind);
|
|
443
|
+
if (mismatch !== undefined) {
|
|
444
|
+
const locator = documentEntry.location ?? documentEntry.block;
|
|
445
|
+
problems.push({
|
|
446
|
+
entryId: entry.entryId,
|
|
447
|
+
error: locator === undefined ? `${mismatch}.` : `${mismatch} (${locator}).`,
|
|
448
|
+
});
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// The same two gates `Project.addBlock` applies, for the same reason: a block that
|
|
453
|
+
// cannot run here must not be installed. Here they become per-entry problems
|
|
454
|
+
// rather than throws, so one unusable block reads as one bad entry.
|
|
455
|
+
this.env.runtimeCapabilities.throwIfIncompatible(blockCfg.featureFlags);
|
|
456
|
+
throwIfMissingServerCapabilities(this.pl, preparedBp.requiredCapabilities);
|
|
457
|
+
|
|
458
|
+
const cachedBp = await cacheBlockPackTemplate(this.pl, preparedBp);
|
|
459
|
+
|
|
460
|
+
// Offered to the block's kind while nothing has been created yet. Every entry is
|
|
461
|
+
// checked, including one whose file omitted `params` — the parser read that as `{}`,
|
|
462
|
+
// which a kind with required fields rejects, and rightly: it would otherwise apply as
|
|
463
|
+
// a block that looks configured and is not.
|
|
464
|
+
const checked = this.env.projectHelper.validateTemplateParamsInVM(
|
|
465
|
+
blockCfg,
|
|
466
|
+
documentEntry.params,
|
|
467
|
+
);
|
|
468
|
+
if (checked.error !== undefined) {
|
|
469
|
+
problems.push({ entryId: entry.entryId, error: checked.error.message });
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The block package's own title, the same thing the add-block UI writes. It is
|
|
474
|
+
// what the user sees for a block whose model derives no title of its own, and it
|
|
475
|
+
// is resolution's to supply — nothing here could reconstruct it.
|
|
476
|
+
prepared.set(entry.entryId, { blockPack: cachedBp, label: entry.title });
|
|
477
|
+
} catch (e) {
|
|
478
|
+
problems.push({
|
|
479
|
+
entryId: entry.entryId,
|
|
480
|
+
error: `This entry's block could not be installed: ${ensureError(e).message}`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (problems.length > 0) return { added: [], problems };
|
|
486
|
+
|
|
487
|
+
const rid = await this.resolveProjectId(id);
|
|
488
|
+
let added: AppliedEntry[] = [];
|
|
489
|
+
try {
|
|
490
|
+
await withProjectAuthored(
|
|
491
|
+
this.env.projectHelper,
|
|
492
|
+
this.pl,
|
|
493
|
+
rid,
|
|
494
|
+
options.author,
|
|
495
|
+
(mut) => {
|
|
496
|
+
added = applyTemplateEntries({
|
|
497
|
+
document,
|
|
498
|
+
placer: mut,
|
|
499
|
+
entries: prepared,
|
|
500
|
+
projectHelper: this.env.projectHelper,
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
// Under the same lock an open session's own mutations take, so an apply and a user
|
|
504
|
+
// editing the project cannot interleave.
|
|
505
|
+
{ name: "applyTemplateToProject", lockId: `project:${id}` },
|
|
506
|
+
);
|
|
507
|
+
} catch (e: unknown) {
|
|
508
|
+
// A statement about the file: the transaction went with the throw, so the project kept
|
|
509
|
+
// none of the blocks the apply had placed, and `added` is empty by construction.
|
|
510
|
+
// Anything else — a backend that refused the write, say — is not about the file and
|
|
511
|
+
// keeps propagating.
|
|
512
|
+
if (!(e instanceof TemplateEntryRejected)) throw e;
|
|
513
|
+
return { added: [], problems: [{ entryId: e.entryId, error: e.message }] };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { added, problems: [] };
|
|
517
|
+
}
|
|
518
|
+
|
|
328
519
|
/** Permanently deletes project from the project list, this will result in
|
|
329
520
|
* destruction of all attached objects, like files, analysis results etc. */
|
|
330
521
|
public async deleteProject(id: ProjectId): Promise<void> {
|
|
@@ -996,7 +996,7 @@ function convertErrorsToStrings(
|
|
|
996
996
|
* the UI (`AddBlockModal`) and direct middle-layer callers (the MCP server's
|
|
997
997
|
* `add_block` tool, programmatic block installs, tests) reject incompatible
|
|
998
998
|
* blocks consistently. */
|
|
999
|
-
function throwIfMissingServerCapabilities(
|
|
999
|
+
export function throwIfMissingServerCapabilities(
|
|
1000
1000
|
pl: PlClient,
|
|
1001
1001
|
requiredCapabilities: readonly string[] | undefined,
|
|
1002
1002
|
): void {
|
package/src/model/args.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, test } from "vitest";
|
|
2
2
|
import canonicalize from "canonicalize";
|
|
3
|
+
import type { ColumnUniversalId } from "@milaboratories/pl-model-common";
|
|
4
|
+
import { canonicalizeJson, createColumnDiscoveredId } from "@milaboratories/pl-model-common";
|
|
3
5
|
import { inferAllReferencedBlocks, outputRef } from "./args";
|
|
4
6
|
|
|
5
7
|
describe("inferAllReferencedBlocks", () => {
|
|
@@ -133,3 +135,26 @@ describe("inferAllReferencedBlocks", () => {
|
|
|
133
135
|
expect(result.upstreams.size).toBe(0);
|
|
134
136
|
});
|
|
135
137
|
});
|
|
138
|
+
|
|
139
|
+
test("a block id in a map KEY is an upstream too", () => {
|
|
140
|
+
// A discovered column's `queriesQualifications` is `Record<PObjectId, …>`, so the id sits in
|
|
141
|
+
// the key. Walking values only lost that edge: the block depended on an upstream the graph
|
|
142
|
+
// never knew about, and the loss was invisible because the rest of the id parsed fine.
|
|
143
|
+
const qual = [{ axis: { name: "sampleId" }, contextDomain: {} }];
|
|
144
|
+
const discovered = createColumnDiscoveredId({
|
|
145
|
+
column: canonicalizeJson(outputRef("A", "clonotypes")) as ColumnUniversalId,
|
|
146
|
+
path: [
|
|
147
|
+
{
|
|
148
|
+
type: "linker",
|
|
149
|
+
column: canonicalizeJson(outputRef("B", "cell-to-clone")) as ColumnUniversalId,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
queriesQualifications: {
|
|
153
|
+
[canonicalizeJson(outputRef("D", "anchor")) as ColumnUniversalId]: qual,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = inferAllReferencedBlocks({ anchor: discovered });
|
|
158
|
+
|
|
159
|
+
expect([...result.upstreams].sort()).toEqual(["A", "B", "D"]);
|
|
160
|
+
});
|
package/src/model/args.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertNever } from "@milaboratories/ts-helpers";
|
|
2
2
|
import type { PlRef } from "@platforma-sdk/model";
|
|
3
|
+
import { peelJsonLayers } from "@milaboratories/pl-model-common";
|
|
3
4
|
|
|
4
5
|
export function outputRef(blockId: string, name: string, requireEnrichments?: boolean): PlRef {
|
|
5
6
|
if (requireEnrichments) return { __isRef: true, blockId, name, requireEnrichments };
|
|
@@ -48,9 +49,15 @@ function addAllReferencedBlocks(result: BlockUpstreams, node: unknown, allowed?:
|
|
|
48
49
|
case "undefined":
|
|
49
50
|
return;
|
|
50
51
|
case "string": {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// A reference can be hiding inside a string under any number of `JSON.stringify`
|
|
53
|
+
// passes, and this walk would otherwise never reach it. Peeling is shared with the
|
|
54
|
+
// column-id remapper so that "how a value hides inside a string" has one
|
|
55
|
+
// definition; what happens at the bottom is not shared, and here it is deliberately
|
|
56
|
+
// broad — anything the peel produced is walked, including a foreign-schema document
|
|
57
|
+
// that merely contains a reference. That breadth is what makes this detector usable
|
|
58
|
+
// as a guard over carriers the template codec declines to touch.
|
|
59
|
+
const peeled = peelJsonLayers(node as string);
|
|
60
|
+
if (peeled !== undefined) addAllReferencedBlocks(result, peeled.value, allowed);
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
56
63
|
case "object": {
|
|
@@ -60,8 +67,13 @@ function addAllReferencedBlocks(result: BlockUpstreams, node: unknown, allowed?:
|
|
|
60
67
|
} else if (Array.isArray(node)) {
|
|
61
68
|
for (const child of node) addAllReferencedBlocks(result, child, allowed);
|
|
62
69
|
} else {
|
|
63
|
-
|
|
70
|
+
// Keys as well as values: a discovered column's `queriesQualifications` is keyed BY
|
|
71
|
+
// column id, so a map key can carry a block id — and dropping the key loses that
|
|
72
|
+
// upstream edge entirely.
|
|
73
|
+
for (const [key, child] of Object.entries(node as object)) {
|
|
74
|
+
addAllReferencedBlocks(result, key, allowed);
|
|
64
75
|
addAllReferencedBlocks(result, child, allowed);
|
|
76
|
+
}
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
return;
|
|
@@ -71,33 +83,6 @@ function addAllReferencedBlocks(result: BlockUpstreams, node: unknown, allowed?:
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
/**
|
|
75
|
-
* Detect a PlRef carried inside a string and hand the decoded value to `onParsed`.
|
|
76
|
-
*
|
|
77
|
-
* A PlRef-as-string is canonical `{...}` optionally wrapped by N `JSON.stringify`
|
|
78
|
-
* passes. Each pass adds a symmetric prefix/suffix made only of `"` and `\`
|
|
79
|
-
* chars (the escape padding) of equal length. The regex below is the strict
|
|
80
|
-
* shape gate — non-ref strings fail at the very first character, so we never
|
|
81
|
-
* scan their body. One pass is peeled per call; deeper nesting is unwrapped
|
|
82
|
-
* via recursion in the caller.
|
|
83
|
-
*/
|
|
84
|
-
const EMBEDDED_REF_RE = /^(?<pre>[\\"]*)\{[\s\S]*?__isRef[\s\S]*\}(?<suf>[\\"]*)$/;
|
|
85
|
-
|
|
86
|
-
function unwrapEmbeddedRef(s: string, onParsed: (value: unknown) => void) {
|
|
87
|
-
const c0 = s.charCodeAt(0);
|
|
88
|
-
if (c0 !== 0x7b /* { */ && c0 !== 0x22 /* " */) return;
|
|
89
|
-
const m = EMBEDDED_REF_RE.exec(s);
|
|
90
|
-
if (m === null) return;
|
|
91
|
-
if (m.groups!.pre.length !== m.groups!.suf.length) return;
|
|
92
|
-
let parsed: unknown;
|
|
93
|
-
try {
|
|
94
|
-
parsed = JSON.parse(s);
|
|
95
|
-
} catch {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (parsed !== s) onParsed(parsed);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
86
|
function recordRef(
|
|
102
87
|
result: BlockUpstreams,
|
|
103
88
|
blockId: string,
|
package/src/model/index.ts
CHANGED
|
@@ -14,3 +14,24 @@ export {
|
|
|
14
14
|
} from "./project_model";
|
|
15
15
|
|
|
16
16
|
export * from "./sharing_model";
|
|
17
|
+
|
|
18
|
+
// The template import path. A caller reads a file, parses it, creates a project and
|
|
19
|
+
// applies the document — so the parser and the report types are as public as the
|
|
20
|
+
// `MiddleLayer.applyTemplateToProject` that consumes them. `BlockPackProvider` is here
|
|
21
|
+
// for the same reason: which registries to consult is the caller's decision.
|
|
22
|
+
export { parseProjectTemplateV1Yaml, type TemplateParseOutcome } from "./template_parser";
|
|
23
|
+
export {
|
|
24
|
+
resolveTemplateEntries,
|
|
25
|
+
parseBlockPackName,
|
|
26
|
+
type BlockPackProvider,
|
|
27
|
+
type KindResolution,
|
|
28
|
+
type ExactResolution,
|
|
29
|
+
type ResolvedEntry,
|
|
30
|
+
type TemplateResolveOutcome,
|
|
31
|
+
} from "./template_resolve";
|
|
32
|
+
export {
|
|
33
|
+
TemplateEntryRejected,
|
|
34
|
+
type AppliedEntry,
|
|
35
|
+
type TemplateApplyProblem,
|
|
36
|
+
type TemplateApplyReport,
|
|
37
|
+
} from "./template_apply";
|
|
@@ -40,6 +40,20 @@ export type MigrationResult =
|
|
|
40
40
|
*/
|
|
41
41
|
type ArgsDeriveResult = { error: string } | { error?: undefined; value: unknown };
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Result of building initial storage from params.
|
|
45
|
+
* Returned by the __pl_storage_initialFromParams VM callback.
|
|
46
|
+
*/
|
|
47
|
+
type ParamsStorageResult =
|
|
48
|
+
| { error: string }
|
|
49
|
+
| { error?: undefined; storageJson: StringifiedJson<BlockStorage> };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Result of checking params against their kind.
|
|
53
|
+
* Returned by the __pl_initializationParams_validate VM callback.
|
|
54
|
+
*/
|
|
55
|
+
type InitializationParamsValidateResult = { error: string } | { error?: undefined };
|
|
56
|
+
|
|
43
57
|
export class ProjectHelper {
|
|
44
58
|
private readonly enrichmentTargetsCache = new LRUCache<
|
|
45
59
|
string,
|
|
@@ -131,6 +145,88 @@ export class ProjectHelper {
|
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Derives this block's template-export params from storage JSON using the VM
|
|
150
|
+
* callback (`__pl_initializationParams_derive`).
|
|
151
|
+
*
|
|
152
|
+
* The template-export counterpart of {@link deriveArgsFromStorage}: instead of
|
|
153
|
+
* the args a workflow runs on, it returns the params that would recreate the
|
|
154
|
+
* block — the inverse of the data model's `init`. Params come back exactly as the block
|
|
155
|
+
* projected them, references included: nothing marks or rewrites them on the way out, so
|
|
156
|
+
* nothing here has to know a kind's params shape or how a reference is spelled.
|
|
157
|
+
*
|
|
158
|
+
* A `{ value: undefined }` result means the block declares no `templateParams`, which only
|
|
159
|
+
* a block built against an older SDK can do. It does not reach a document: the export walk
|
|
160
|
+
* requires a mapping and reports anything else as that block's problem, so an entry always
|
|
161
|
+
* carries params.
|
|
162
|
+
*
|
|
163
|
+
* Unlike {@link derivePrerunArgsFromStorage}, a failure here is surfaced rather
|
|
164
|
+
* than swallowed — a prerun that cannot derive args just skips a block in
|
|
165
|
+
* staging, whereas an export that silently drops a block produces a template
|
|
166
|
+
* that does not describe the project.
|
|
167
|
+
*
|
|
168
|
+
* @param blockConfig The block configuration (provides the model code)
|
|
169
|
+
* @param storageJson Storage as JSON string
|
|
170
|
+
* @returns The derived params with identifiers wrapped, `undefined` if the block declares
|
|
171
|
+
* no lambda, or an error if derivation failed
|
|
172
|
+
*/
|
|
173
|
+
public deriveTemplateParamsFromStorage(
|
|
174
|
+
blockConfig: BlockConfig,
|
|
175
|
+
storageJson: string,
|
|
176
|
+
): ResultOrError<unknown> {
|
|
177
|
+
if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) {
|
|
178
|
+
return {
|
|
179
|
+
error: new Error(
|
|
180
|
+
"deriveTemplateParamsFromStorage is only supported for model API version 2",
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const callback =
|
|
186
|
+
blockConfig.blockLifecycleCallbacks[BlockStorageFacadeCallbacks.InitializationParamsDerive];
|
|
187
|
+
|
|
188
|
+
// A model built before this callback existed simply has no entry for it. That is
|
|
189
|
+
// NOT the same as a block declaring no `templateParams`: the block may well have
|
|
190
|
+
// params, we just have no way to ask for them. Reporting it as `undefined` params
|
|
191
|
+
// would export the block stripped of its configuration and quietly rebuild a
|
|
192
|
+
// differently-configured project, so it has to be an error.
|
|
193
|
+
// The message names the one action available to whoever pressed Export. It
|
|
194
|
+
// deliberately says nothing about SDKs or callbacks: the person reading it did
|
|
195
|
+
// not build this block and cannot change how it was built.
|
|
196
|
+
if (callback === undefined) {
|
|
197
|
+
return {
|
|
198
|
+
error: new Error(
|
|
199
|
+
"This version of the block cannot be written to a template. Update the block " +
|
|
200
|
+
"to a newer version and export again.",
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const result = executeSingleLambda(
|
|
207
|
+
this.quickJs,
|
|
208
|
+
callback,
|
|
209
|
+
extractCodeWithInfo(blockConfig),
|
|
210
|
+
storageJson,
|
|
211
|
+
) as ArgsDeriveResult;
|
|
212
|
+
|
|
213
|
+
if (result.error !== undefined) {
|
|
214
|
+
return { error: new Error(result.error) };
|
|
215
|
+
}
|
|
216
|
+
return { value: result.value };
|
|
217
|
+
} catch (e) {
|
|
218
|
+
const cause = ensureError(e);
|
|
219
|
+
// The reason goes in the message, not only in `cause`: this error is rendered
|
|
220
|
+
// into a per-block export problem and shown to whoever triggered the export,
|
|
221
|
+
// and every layer between here and there carries only `message`.
|
|
222
|
+
return {
|
|
223
|
+
error: new Error(`Template params derivation from storage failed: ${cause.message}`, {
|
|
224
|
+
cause,
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
134
230
|
private calculateEnrichmentTargets(req: EnrichmentTargetsRequest): PlRef[] | undefined {
|
|
135
231
|
const blockConfig = req.blockConfig();
|
|
136
232
|
if (blockConfig.enrichmentTargets === undefined) return undefined;
|
|
@@ -191,6 +287,148 @@ export class ProjectHelper {
|
|
|
191
287
|
}
|
|
192
288
|
}
|
|
193
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Checks a template entry's params against the block's kind, creating nothing.
|
|
292
|
+
*
|
|
293
|
+
* The pre-flight half of {@link getInitialStorageFromParamsInVM}: run once per entry
|
|
294
|
+
* before a template is applied, so params a kind rejects are reported against the
|
|
295
|
+
* entry that carries them while there is still no project. Skipping it is safe —
|
|
296
|
+
* initialization runs the same check — but then the report arrives after earlier
|
|
297
|
+
* entries have already been created.
|
|
298
|
+
*
|
|
299
|
+
* Every kind declares a parser, so a pass here means the params were checked against
|
|
300
|
+
* the contract — not merely that they were JSON.
|
|
301
|
+
*
|
|
302
|
+
* A block whose model predates the callback passes unchecked rather than failing.
|
|
303
|
+
* Unlike initialization, this method creates nothing, so there is nothing to get wrong
|
|
304
|
+
* by proceeding, and such a block is refused outright at the point it is applied.
|
|
305
|
+
*
|
|
306
|
+
* @param blockConfig The block configuration (provides the model code)
|
|
307
|
+
* @param params The entry's params. Reference ids may be placeholders: what is being
|
|
308
|
+
* checked is the shape of the params, not what they point at
|
|
309
|
+
* @returns Nothing, or why the params were rejected
|
|
310
|
+
*/
|
|
311
|
+
public validateTemplateParamsInVM(
|
|
312
|
+
blockConfig: BlockConfig,
|
|
313
|
+
params: unknown,
|
|
314
|
+
): ResultOrError<undefined> {
|
|
315
|
+
if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) {
|
|
316
|
+
return {
|
|
317
|
+
error: new Error("validateTemplateParamsInVM is only supported for model API version 2"),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const callback =
|
|
322
|
+
blockConfig.blockLifecycleCallbacks[BlockStorageFacadeCallbacks.InitializationParamsValidate];
|
|
323
|
+
if (callback === undefined) return { value: undefined };
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const result = executeSingleLambda(
|
|
327
|
+
this.quickJs,
|
|
328
|
+
callback,
|
|
329
|
+
extractCodeWithInfo(blockConfig),
|
|
330
|
+
JSON.stringify(params ?? {}),
|
|
331
|
+
) as InitializationParamsValidateResult;
|
|
332
|
+
|
|
333
|
+
if (result.error !== undefined) return { error: new Error(result.error) };
|
|
334
|
+
return { value: undefined };
|
|
335
|
+
} catch (e) {
|
|
336
|
+
const cause = ensureError(e);
|
|
337
|
+
return { error: new Error(`Params check failed to run: ${cause.message}`, { cause }) };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Creates initial BlockStorage for a block being created from template params.
|
|
343
|
+
*
|
|
344
|
+
* The inverse of {@link deriveTemplateParamsFromStorage}, and the reason a block
|
|
345
|
+
* can be created by anything other than the UI: it hands the params to the
|
|
346
|
+
* block's own init factory inside the model VM, so the resulting storage is
|
|
347
|
+
* whatever that block considers a correctly-initialized state.
|
|
348
|
+
*
|
|
349
|
+
* **Pointing the entry's references at this project happens in the same call**, before the
|
|
350
|
+
* factory sees them — inside the block's bundle, because recognizing a reference means
|
|
351
|
+
* knowing the reference system and a template engine deliberately does not. Params reach
|
|
352
|
+
* here exactly as the file held them.
|
|
353
|
+
*
|
|
354
|
+
* One call and not two: every VM call builds a fresh runtime and re-evaluates the whole
|
|
355
|
+
* model bundle, so asking the block to relocate and then asking it to initialize would parse
|
|
356
|
+
* it twice per entry, for an intermediate value nothing else reads.
|
|
357
|
+
*
|
|
358
|
+
* `blockIds` holds the entries created so far, so an id it does not name is left as it is —
|
|
359
|
+
* a reference to an entry further down the file keeps pointing at nothing, and the applied
|
|
360
|
+
* block reports itself as missing references rather than being wired to a block below it.
|
|
361
|
+
*
|
|
362
|
+
* @param blockConfig The block configuration (provides the model code)
|
|
363
|
+
* @param params The entry's params, as the document held them
|
|
364
|
+
* @param blockIds template-local entry id → the block id that entry was given
|
|
365
|
+
* @returns The initial storage as JSON string, or why the params yield none
|
|
366
|
+
*/
|
|
367
|
+
public getInitialStorageFromParamsInVM(
|
|
368
|
+
blockConfig: BlockConfig,
|
|
369
|
+
params: unknown,
|
|
370
|
+
blockIds: ReadonlyMap<string, string>,
|
|
371
|
+
): ResultOrError<string> {
|
|
372
|
+
if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) {
|
|
373
|
+
return {
|
|
374
|
+
error: new Error(
|
|
375
|
+
"getInitialStorageFromParamsInVM is only supported for model API version 2",
|
|
376
|
+
),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const callback =
|
|
381
|
+
blockConfig.blockLifecycleCallbacks[BlockStorageFacadeCallbacks.StorageInitialFromParams];
|
|
382
|
+
|
|
383
|
+
// A model built before this callback existed has no entry for it. Falling back to the
|
|
384
|
+
// params-less initializer is not an option: it would produce a default-configured block
|
|
385
|
+
// that looks like a successful apply, so the block the user gets would silently differ
|
|
386
|
+
// from the one the template describes. Nor could its params be used as written — they name
|
|
387
|
+
// the blocks of the project the template came from, and only this callback knows which of
|
|
388
|
+
// the values in there are references at all.
|
|
389
|
+
//
|
|
390
|
+
// The message offers the two actions available to whoever applied the file. The
|
|
391
|
+
// second one is the reason this branch is reachable at all: kind resolution only
|
|
392
|
+
// ever returns a block that declares a kind, and such a block is new enough to
|
|
393
|
+
// support this — but an entry may pin an exact block version instead, bypassing
|
|
394
|
+
// resolution, and that pin can name anything ever published.
|
|
395
|
+
if (callback === undefined) {
|
|
396
|
+
return {
|
|
397
|
+
error: new Error(
|
|
398
|
+
"This version of the block cannot be created from a template. Use a newer " +
|
|
399
|
+
"version of the block, or remove the pinned block version from the template " +
|
|
400
|
+
"entry so a supported one is chosen automatically.",
|
|
401
|
+
),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const result = executeSingleLambda(
|
|
407
|
+
this.quickJs,
|
|
408
|
+
callback,
|
|
409
|
+
extractCodeWithInfo(blockConfig),
|
|
410
|
+
// Params cross the VM boundary as text, like storage does. `undefined` would
|
|
411
|
+
// stringify to nothing at all, and an entry with no params must go through
|
|
412
|
+
// the params-less initializer rather than reaching this method.
|
|
413
|
+
JSON.stringify(params ?? {}),
|
|
414
|
+
JSON.stringify(Object.fromEntries(blockIds)),
|
|
415
|
+
) as ParamsStorageResult;
|
|
416
|
+
|
|
417
|
+
if (result.error !== undefined) return { error: new Error(result.error) };
|
|
418
|
+
return { value: result.storageJson };
|
|
419
|
+
} catch (e) {
|
|
420
|
+
const cause = ensureError(e);
|
|
421
|
+
// The reason goes in the message, not only in `cause`: this error becomes a
|
|
422
|
+
// per-entry apply problem shown to whoever triggered the import, and every
|
|
423
|
+
// layer in between carries only `message`.
|
|
424
|
+
return {
|
|
425
|
+
error: new Error(`Initial storage creation from params failed: ${cause.message}`, {
|
|
426
|
+
cause,
|
|
427
|
+
}),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
194
432
|
/**
|
|
195
433
|
* Applies a state update using VM-based transformation.
|
|
196
434
|
* This calls the model's `__pl_storage_applyUpdate` callback which:
|