@milaboratories/pl-middle-layer 1.66.18 → 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 +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
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { getQuickJS, type QuickJSWASMModule } from "quickjs-emscripten";
|
|
3
|
+
import { BlockStorageFacadeCallbacks } from "@platforma-sdk/model";
|
|
4
|
+
import type {
|
|
5
|
+
BlockKindSelectorReference,
|
|
6
|
+
ProjectTemplateV1,
|
|
7
|
+
ProjectTemplateV1Entry,
|
|
8
|
+
} from "@milaboratories/pl-model-common";
|
|
9
|
+
import { PROJECT_TEMPLATE_SCHEMA_V1, createPlRef } from "@milaboratories/pl-model-common";
|
|
10
|
+
import { ProjectHelper } from "../model/project_helper";
|
|
11
|
+
import type { BlockPackSpecPrepared } from "../model";
|
|
12
|
+
import type { Block } from "../model/project_model";
|
|
13
|
+
import type { NewBlockSpec } from "./project";
|
|
14
|
+
import type { PreparedTemplateEntry } from "./template_construct";
|
|
15
|
+
import { TemplateEntryRejected } from "../model/template_apply";
|
|
16
|
+
import { applyTemplateEntries } from "./template_construct";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Applying a document against a recording placer.
|
|
20
|
+
*
|
|
21
|
+
* A real mutator would need a backend; the one method construction uses does not, so the
|
|
22
|
+
* placer is faked and everything else here is real — a real `ProjectHelper`, a real model VM,
|
|
23
|
+
* real block code. What is being checked is the seam between an entry and a block: which id
|
|
24
|
+
* it got, which storage its params produced, what a rejection does, and how far an apply gets
|
|
25
|
+
* before it stops.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const HANDLE = BlockStorageFacadeCallbacks.StorageInitialFromParams;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A block that relocates its references and echoes the result as its storage.
|
|
32
|
+
*
|
|
33
|
+
* Both halves of the real callback, in the order the real one does them: repoint, then
|
|
34
|
+
* initialize. Only `PlRef`s here — the real relocation recognizes all five identifier forms at
|
|
35
|
+
* any depth and is unit-tested where it lives (`pl-model-common`). What these tests need is
|
|
36
|
+
* that the seam works: the map arrives, and what the block returns is what it is created with.
|
|
37
|
+
*/
|
|
38
|
+
const relocatingEchoModel = `(paramsJson, blockIdsJson) => {
|
|
39
|
+
const ids = JSON.parse(blockIdsJson);
|
|
40
|
+
const keys = (o) => Object.keys(o).sort().join(",");
|
|
41
|
+
const walk = (node) => {
|
|
42
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
43
|
+
if (node !== null && typeof node === "object") {
|
|
44
|
+
// The readable spelling first, as the real one does, so both arrive the same way.
|
|
45
|
+
if (keys(node) === "block,name") node = { __isRef: true, blockId: node.block, name: node.name };
|
|
46
|
+
if (node.__isRef === true) return { ...node, blockId: ids[node.blockId] ?? node.blockId };
|
|
47
|
+
return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, walk(v)]));
|
|
48
|
+
}
|
|
49
|
+
return node;
|
|
50
|
+
};
|
|
51
|
+
return { storageJson: JSON.stringify(walk(JSON.parse(paramsJson))) };
|
|
52
|
+
}`;
|
|
53
|
+
|
|
54
|
+
/** A v4 block whose params-to-storage callback body is `body`. */
|
|
55
|
+
function preparedBlock(
|
|
56
|
+
body: string,
|
|
57
|
+
options: { declareCallback?: boolean } = {},
|
|
58
|
+
): BlockPackSpecPrepared {
|
|
59
|
+
const { declareCallback = true } = options;
|
|
60
|
+
return {
|
|
61
|
+
type: "prepared",
|
|
62
|
+
config: {
|
|
63
|
+
code: {
|
|
64
|
+
type: "plain",
|
|
65
|
+
content: `globalThis.cfgRenderCtx.callbackRegistry[${JSON.stringify(HANDLE)}] = ${body};`,
|
|
66
|
+
},
|
|
67
|
+
v4: {
|
|
68
|
+
sdkVersion: "1.0.0",
|
|
69
|
+
renderingMode: "Heavy",
|
|
70
|
+
outputs: {},
|
|
71
|
+
sections: [],
|
|
72
|
+
// A block that predates the params callback still declares the older ones, and
|
|
73
|
+
// it has to: `extractConfig` fills every handle in for a v4 block whose set is
|
|
74
|
+
// empty, which would hide this case entirely.
|
|
75
|
+
blockLifecycleCallbacks: {
|
|
76
|
+
[BlockStorageFacadeCallbacks.StorageInitial]: {
|
|
77
|
+
handle: BlockStorageFacadeCallbacks.StorageInitial,
|
|
78
|
+
},
|
|
79
|
+
...(declareCallback ? { [HANDLE]: { handle: HANDLE } } : {}),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
} as unknown as BlockPackSpecPrepared;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A block on the previous model API, which cannot be initialized from params at all. */
|
|
87
|
+
function legacyPreparedBlock(): BlockPackSpecPrepared {
|
|
88
|
+
return {
|
|
89
|
+
type: "prepared",
|
|
90
|
+
config: {
|
|
91
|
+
code: { type: "plain", content: "" },
|
|
92
|
+
v3: {
|
|
93
|
+
sdkVersion: "1.0.0",
|
|
94
|
+
renderingMode: "Heavy",
|
|
95
|
+
outputs: {},
|
|
96
|
+
sections: [],
|
|
97
|
+
initialArgs: {},
|
|
98
|
+
inputsValid: "inputsValid",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
} as unknown as BlockPackSpecPrepared;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Echoes its params back as the block's storage, relocating references on the way. */
|
|
105
|
+
const echoModel = relocatingEchoModel;
|
|
106
|
+
|
|
107
|
+
type Placement = { block: Block; spec: NewBlockSpec };
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param throwOn Block label to refuse to place, as an exception — the mutator's way of
|
|
111
|
+
* failing
|
|
112
|
+
*/
|
|
113
|
+
function recordingPlacer(throwOn?: string) {
|
|
114
|
+
const placements: Placement[] = [];
|
|
115
|
+
return {
|
|
116
|
+
placements,
|
|
117
|
+
placer: {
|
|
118
|
+
addBlock: (block: Block, spec: NewBlockSpec) => {
|
|
119
|
+
if (block.label === throwOn) throw new Error("structure is broken");
|
|
120
|
+
placements.push({ block, spec });
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function entryMap(
|
|
127
|
+
entries: Record<string, BlockPackSpecPrepared>,
|
|
128
|
+
): Map<string, PreparedTemplateEntry> {
|
|
129
|
+
return new Map(
|
|
130
|
+
Object.entries(entries).map(([id, blockPack]) => [id, { blockPack, label: `Block ${id}` }]),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The storage a placement carries, parsed. */
|
|
135
|
+
function storageOf(placement: Placement): unknown {
|
|
136
|
+
if (placement.spec.storageMode !== "fromModel") throw new Error("expected fromModel");
|
|
137
|
+
const { initialStorage } = placement.spec;
|
|
138
|
+
if (initialStorage === undefined) throw new Error("expected seeded storage");
|
|
139
|
+
return JSON.parse(initialStorage);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let quickJs: QuickJSWASMModule;
|
|
143
|
+
|
|
144
|
+
beforeAll(async () => {
|
|
145
|
+
quickJs = await getQuickJS();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const KIND = "@platforma-open/milaboratories.demo.kind@^1.0.0" as BlockKindSelectorReference;
|
|
149
|
+
|
|
150
|
+
/** An entry as the parser hands it over: `params` settled, absent read as `{}`. */
|
|
151
|
+
const entry = (id: string, params: Record<string, unknown> = {}): ProjectTemplateV1Entry => ({
|
|
152
|
+
id,
|
|
153
|
+
kind: KIND,
|
|
154
|
+
params,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const documentOf = (...blocks: ProjectTemplateV1Entry[]): ProjectTemplateV1 => ({
|
|
158
|
+
schema: PROJECT_TEMPLATE_SCHEMA_V1,
|
|
159
|
+
blocks,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/** One apply, with ids named as a counter so the assertions can spell them out. */
|
|
163
|
+
function applyOver(
|
|
164
|
+
entries: Map<string, PreparedTemplateEntry>,
|
|
165
|
+
placer: { addBlock: (block: Block, spec: NewBlockSpec) => void },
|
|
166
|
+
document: ProjectTemplateV1,
|
|
167
|
+
) {
|
|
168
|
+
let n = 0;
|
|
169
|
+
return applyTemplateEntries({
|
|
170
|
+
document,
|
|
171
|
+
placer,
|
|
172
|
+
projectHelper: new ProjectHelper(quickJs),
|
|
173
|
+
entries,
|
|
174
|
+
newBlockId: () => `block-${++n}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The rejection an apply throws, for tests that assert which entry stopped it.
|
|
180
|
+
*
|
|
181
|
+
* Note what these tests cannot show: that the placements made before the rejection are
|
|
182
|
+
* discarded. That is the transaction's doing, and the placer here is a recording fake with no
|
|
183
|
+
* transaction — so it keeps what it was told, while the real apply keeps nothing.
|
|
184
|
+
*/
|
|
185
|
+
function rejectionFrom(run: () => unknown): TemplateEntryRejected {
|
|
186
|
+
try {
|
|
187
|
+
run();
|
|
188
|
+
} catch (e: unknown) {
|
|
189
|
+
if (e instanceof TemplateEntryRejected) return e;
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
throw new Error("expected the apply to reject an entry, but it succeeded");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
describe("applyTemplateEntries", () => {
|
|
196
|
+
test("an entry's params become the storage its block starts with", () => {
|
|
197
|
+
// The whole reason a template can describe a configured block: the params go
|
|
198
|
+
// through the block's own model, so what lands is what that block considers a
|
|
199
|
+
// correctly initialized state.
|
|
200
|
+
const { placer, placements } = recordingPlacer();
|
|
201
|
+
|
|
202
|
+
const added = applyOver(
|
|
203
|
+
entryMap({ a: preparedBlock(echoModel) }),
|
|
204
|
+
placer,
|
|
205
|
+
documentOf(entry("a", { numbers: [3, 1, 2] })),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(added).toEqual([{ templateLocalId: "a", blockId: "block-1" }]);
|
|
209
|
+
expect(storageOf(placements[0])).toEqual({ numbers: [3, 1, 2] });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("an entry with no params goes through the params path as `{}`", () => {
|
|
213
|
+
// Not routed around it. The two produce the same block anyway — both reach the same
|
|
214
|
+
// init factory — but only this way is the entry checked against its kind, so an
|
|
215
|
+
// omitted key cannot be a way to apply params the contract would have rejected.
|
|
216
|
+
const { placer, placements } = recordingPlacer();
|
|
217
|
+
|
|
218
|
+
applyOver(entryMap({ a: preparedBlock(echoModel) }), placer, documentOf(entry("a")));
|
|
219
|
+
|
|
220
|
+
expect(placements[0].spec).toEqual({
|
|
221
|
+
storageMode: "fromModel",
|
|
222
|
+
blockPack: expect.anything(),
|
|
223
|
+
initialStorage: expect.anything(),
|
|
224
|
+
});
|
|
225
|
+
expect(storageOf(placements[0])).toEqual({});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("references are repointed by the block, to the blocks already placed", () => {
|
|
229
|
+
// The payoff of the whole import path: the second entry's params name a template-local id
|
|
230
|
+
// in the file and reach the block as a reference to the id the first block actually got.
|
|
231
|
+
// Nothing here recognized that reference — the block's own bundle did, which is why the
|
|
232
|
+
// params travelled from the file untouched.
|
|
233
|
+
const { placer, placements } = recordingPlacer();
|
|
234
|
+
const params = { input: createPlRef("samples", "reads") };
|
|
235
|
+
|
|
236
|
+
applyOver(
|
|
237
|
+
entryMap({ samples: preparedBlock(echoModel), align: preparedBlock(echoModel) }),
|
|
238
|
+
placer,
|
|
239
|
+
documentOf(entry("samples"), entry("align", params)),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(placements[0].block.id).toBe("block-1");
|
|
243
|
+
expect(storageOf(placements[1])).toEqual({ input: createPlRef("block-1", "reads") });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("the readable spelling arrives as the same reference the long one does", () => {
|
|
247
|
+
// What a hand-written file gets to write. It is expanded in the block's bundle, before its
|
|
248
|
+
// ids are repointed, so it reaches init indistinguishable from a reference the file spelled
|
|
249
|
+
// out in full — which is what keeps a kind's contract written against `PlRef` alone.
|
|
250
|
+
const { placer, placements } = recordingPlacer();
|
|
251
|
+
|
|
252
|
+
applyOver(
|
|
253
|
+
entryMap({ samples: preparedBlock(echoModel), align: preparedBlock(echoModel) }),
|
|
254
|
+
placer,
|
|
255
|
+
documentOf(entry("samples"), entry("align", { input: { block: "samples", name: "reads" } })),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(storageOf(placements[1])).toEqual({ input: createPlRef("block-1", "reads") });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("entries are created in file order, and each pairs with the id it got", () => {
|
|
262
|
+
// File order is instantiation order, which is what makes one forward pass enough.
|
|
263
|
+
const { placer, placements } = recordingPlacer();
|
|
264
|
+
|
|
265
|
+
const added = applyOver(
|
|
266
|
+
entryMap({
|
|
267
|
+
a: preparedBlock(echoModel),
|
|
268
|
+
b: preparedBlock(echoModel),
|
|
269
|
+
c: preparedBlock(echoModel),
|
|
270
|
+
}),
|
|
271
|
+
placer,
|
|
272
|
+
documentOf(entry("a"), entry("b"), entry("c")),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(placements.map((p) => p.block.id)).toEqual(["block-1", "block-2", "block-3"]);
|
|
276
|
+
expect(added).toEqual([
|
|
277
|
+
{ templateLocalId: "a", blockId: "block-1" },
|
|
278
|
+
{ templateLocalId: "b", blockId: "block-2" },
|
|
279
|
+
{ templateLocalId: "c", blockId: "block-3" },
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("an empty document is a successful apply that creates nothing", () => {
|
|
284
|
+
// An exported empty project round-trips to an empty project, not to a failure.
|
|
285
|
+
const { placer, placements } = recordingPlacer();
|
|
286
|
+
|
|
287
|
+
expect(applyOver(entryMap({}), placer, documentOf())).toEqual([]);
|
|
288
|
+
expect(placements).toHaveLength(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("the block is placed under the caller's label, in the block's rendering mode", () => {
|
|
292
|
+
const { placer, placements } = recordingPlacer();
|
|
293
|
+
|
|
294
|
+
applyOver(entryMap({ a: preparedBlock(echoModel) }), placer, documentOf(entry("a")));
|
|
295
|
+
|
|
296
|
+
expect(placements[0].block).toEqual({
|
|
297
|
+
id: "block-1",
|
|
298
|
+
label: "Block a",
|
|
299
|
+
renderingMode: "Heavy",
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("params the block declines are reported, and nothing is placed", () => {
|
|
304
|
+
// The expected failure for a hand-written file. The message is the block's own.
|
|
305
|
+
const { placer, placements } = recordingPlacer();
|
|
306
|
+
|
|
307
|
+
const rejection = rejectionFrom(() =>
|
|
308
|
+
applyOver(
|
|
309
|
+
entryMap({ a: preparedBlock('() => ({ error: "numbers must not be empty" })') }),
|
|
310
|
+
placer,
|
|
311
|
+
documentOf(entry("a", { numbers: [] })),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
expect(rejection.entryId).toBe("a");
|
|
316
|
+
expect(rejection.message).toBe("numbers must not be empty");
|
|
317
|
+
expect(placements).toHaveLength(0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("it stops at the first entry it cannot create, and names it", () => {
|
|
321
|
+
// Nothing after the failure is attempted: those entries may reference the one that
|
|
322
|
+
// failed, and the whole apply is discarded anyway.
|
|
323
|
+
const { placer, placements } = recordingPlacer();
|
|
324
|
+
|
|
325
|
+
const rejection = rejectionFrom(() =>
|
|
326
|
+
applyOver(
|
|
327
|
+
entryMap({
|
|
328
|
+
a: preparedBlock(echoModel),
|
|
329
|
+
b: preparedBlock('() => ({ error: "params rejected" })'),
|
|
330
|
+
c: preparedBlock(echoModel),
|
|
331
|
+
}),
|
|
332
|
+
placer,
|
|
333
|
+
documentOf(entry("a"), entry("b"), entry("c")),
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
expect(rejection.entryId).toBe("b");
|
|
338
|
+
expect(placements.map((p) => p.block.id)).toEqual(["block-1"]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("a reference the map does not name travels as written, and the block is placed", () => {
|
|
342
|
+
// `b` is not in the map — it names no entry at all here, and an entry listed below this
|
|
343
|
+
// one would look the same. Either way the block is created wired to an id that means
|
|
344
|
+
// nothing in this project, and reports itself as missing references. That is the ordering
|
|
345
|
+
// rule's only enforcement now: nothing rejects such a document up front.
|
|
346
|
+
const { placer, placements } = recordingPlacer();
|
|
347
|
+
const params = { input: createPlRef("b", "out") };
|
|
348
|
+
|
|
349
|
+
applyOver(entryMap({ a: preparedBlock(echoModel) }), placer, documentOf(entry("a", params)));
|
|
350
|
+
|
|
351
|
+
expect(placements).toHaveLength(1);
|
|
352
|
+
expect(storageOf(placements[0])).toEqual({ input: createPlRef("b", "out") });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("a block too old to be initialized from params is refused", () => {
|
|
356
|
+
// It could be added — that is the danger. Creating it would ignore everything the
|
|
357
|
+
// template said about it and still look like a successful apply.
|
|
358
|
+
const { placer, placements } = recordingPlacer();
|
|
359
|
+
|
|
360
|
+
const rejection = rejectionFrom(() =>
|
|
361
|
+
applyOver(
|
|
362
|
+
entryMap({ a: legacyPreparedBlock() }),
|
|
363
|
+
placer,
|
|
364
|
+
documentOf(entry("a", { numbers: [1] })),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(rejection.entryId).toBe("a");
|
|
369
|
+
expect(rejection.message).toBe(
|
|
370
|
+
"This version of the block is too old to be created from a template. Use a newer " +
|
|
371
|
+
"version of the block, or remove the pinned block version from this entry so a " +
|
|
372
|
+
"supported one is chosen automatically.",
|
|
373
|
+
);
|
|
374
|
+
expect(placements).toHaveLength(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("a block too old for kinds is refused even with no params of its own", () => {
|
|
378
|
+
// Nothing to ignore here, and it would be created correctly — but every entry names
|
|
379
|
+
// a kind, and a block predating the facade implements none, so it is not the block
|
|
380
|
+
// the entry asked for.
|
|
381
|
+
const { placer } = recordingPlacer();
|
|
382
|
+
|
|
383
|
+
const rejection = rejectionFrom(() =>
|
|
384
|
+
applyOver(entryMap({ a: legacyPreparedBlock() }), placer, documentOf(entry("a"))),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(rejection.entryId).toBe("a");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("a current block whose model predates the callback reports the model's way out", () => {
|
|
391
|
+
// A different failure from the one above: the block is on the current facade, its
|
|
392
|
+
// model just has no params initializer. The message comes from the helper and must
|
|
393
|
+
// reach the caller as-is.
|
|
394
|
+
const { placer } = recordingPlacer();
|
|
395
|
+
|
|
396
|
+
const rejection = rejectionFrom(() =>
|
|
397
|
+
applyOver(
|
|
398
|
+
entryMap({ a: preparedBlock(echoModel, { declareCallback: false }) }),
|
|
399
|
+
placer,
|
|
400
|
+
documentOf(entry("a", { numbers: [1] })),
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
expect(rejection.message).toBe(
|
|
405
|
+
"This version of the block cannot be created from a template. Use a newer version " +
|
|
406
|
+
"of the block, or remove the pinned block version from the template entry so a " +
|
|
407
|
+
"supported one is chosen automatically.",
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("an entry nothing was prepared for is reported", () => {
|
|
412
|
+
// Resolution covers every entry or the apply never starts, so this is a caller error —
|
|
413
|
+
// reported rather than thrown, so it reads like any other bad entry.
|
|
414
|
+
const { placer } = recordingPlacer();
|
|
415
|
+
|
|
416
|
+
const rejection = rejectionFrom(() =>
|
|
417
|
+
applyOver(entryMap({ a: preparedBlock(echoModel) }), placer, documentOf(entry("ghost"))),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(rejection.entryId).toBe("ghost");
|
|
421
|
+
expect(rejection.message).toBe("No block was prepared for entry 'ghost'.");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("a failure to place a block is not swallowed", () => {
|
|
425
|
+
// Deliberately unlike the failures above. By this point the mutator holds half a
|
|
426
|
+
// change, so the only safe outcome is to lose the transaction — reporting it would
|
|
427
|
+
// commit something no one wrote.
|
|
428
|
+
const { placer } = recordingPlacer("Block a");
|
|
429
|
+
|
|
430
|
+
expect(() =>
|
|
431
|
+
applyOver(entryMap({ a: preparedBlock(echoModel) }), placer, documentOf(entry("a"))),
|
|
432
|
+
).toThrow("structure is broken");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("an entry that references a rejected one is never reached", () => {
|
|
436
|
+
// Why the id map can record an entry before its block is placed: a rejection ends the
|
|
437
|
+
// apply, so there is no surviving project in which a later entry points at a block that
|
|
438
|
+
// was never created.
|
|
439
|
+
const { placer, placements } = recordingPlacer();
|
|
440
|
+
const params = { input: createPlRef("a", "out") };
|
|
441
|
+
|
|
442
|
+
const rejection = rejectionFrom(() =>
|
|
443
|
+
applyOver(
|
|
444
|
+
entryMap({
|
|
445
|
+
a: preparedBlock('() => ({ error: "no" })'),
|
|
446
|
+
b: preparedBlock(echoModel),
|
|
447
|
+
}),
|
|
448
|
+
placer,
|
|
449
|
+
documentOf(entry("a", {}), entry("b", params)),
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(rejection.entryId).toBe("a");
|
|
454
|
+
expect(placements).toHaveLength(0);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { BLOCK_STORAGE_FACADE_VERSION, extractConfig } from "@platforma-sdk/model";
|
|
2
|
+
import type { ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
3
|
+
import type { BlockPackSpecPrepared } from "../model";
|
|
4
|
+
import type { ProjectHelper } from "../model/project_helper";
|
|
5
|
+
import type { AppliedEntry } from "../model/template_apply";
|
|
6
|
+
import { TemplateEntryRejected } from "../model/template_apply";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import type { Block } from "../model/project_model";
|
|
9
|
+
import type { NewBlockSpec } from "./project";
|
|
10
|
+
|
|
11
|
+
/** What construction needs to know about one entry, once its block has been located. */
|
|
12
|
+
export type PreparedTemplateEntry = {
|
|
13
|
+
/** The block pack, already prepared and template-cached. */
|
|
14
|
+
readonly blockPack: BlockPackSpecPrepared;
|
|
15
|
+
/**
|
|
16
|
+
* The label to place the block under: the block package's published title.
|
|
17
|
+
*
|
|
18
|
+
* A template names no block instances, so this cannot come from the file — and must
|
|
19
|
+
* not be derived from it, since an exported template names its entries by the source
|
|
20
|
+
* project's block ids. It reaches here from resolution, the only stage that talks to
|
|
21
|
+
* a registry.
|
|
22
|
+
*
|
|
23
|
+
* A block whose model derives a title shows that instead (`project_overview.ts`
|
|
24
|
+
* prefers the derived one), so for most blocks this is invisible. For the ones that
|
|
25
|
+
* derive none it is the name the user reads.
|
|
26
|
+
*/
|
|
27
|
+
readonly label: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The one method construction needs from a project mutator. */
|
|
31
|
+
export type BlockPlacer = {
|
|
32
|
+
addBlock: (block: Block, spec: NewBlockSpec, before?: string) => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Apply a `template-v1` document: create each entry's block, in file order.
|
|
37
|
+
*
|
|
38
|
+
* Everything slow is already done by the time this runs: kinds resolved, block packs
|
|
39
|
+
* prepared and template-cached, the project created and a mutator open. What is left per
|
|
40
|
+
* entry is in-memory work — assign an id, redirect the entry's references against the ids
|
|
41
|
+
* already handed out, ask the block's own model for the storage those params imply, and
|
|
42
|
+
* place the block. All of it inside the caller's single transaction.
|
|
43
|
+
*
|
|
44
|
+
* File order is instantiation order, and a reference to a later entry is rejected before
|
|
45
|
+
* this point, so one forward pass is enough: by the time an entry is created, every entry it
|
|
46
|
+
* references already exists and already has an id. Blocks are appended in the order they are
|
|
47
|
+
* added, so nothing here passes a `before`.
|
|
48
|
+
*
|
|
49
|
+
* **All or nothing.** An entry this project cannot honour throws
|
|
50
|
+
* {@link TemplateEntryRejected}, which leaves the caller's transaction uncommitted, so the
|
|
51
|
+
* project keeps none of the blocks this call placed. A half-applied project is not a useful
|
|
52
|
+
* result: its tail is missing, so whatever the missing entries were supposed to feed is wired
|
|
53
|
+
* to nothing, and the user cannot tell which of the blocks present were configured by the
|
|
54
|
+
* file and which they would have to fix. Failing whole also means the reader gets one
|
|
55
|
+
* statement about their file rather than a project to reconcile.
|
|
56
|
+
*
|
|
57
|
+
* @param entries Prepared entries by template-local id. Resolution covers every entry
|
|
58
|
+
* or the apply does not start, so a missing one is a caller error rather than a
|
|
59
|
+
* property of the file
|
|
60
|
+
* @param newBlockId Source of project-local block ids. Defaults to random UUIDs, the same
|
|
61
|
+
* ids `Project.addBlock` would have generated on its own; injectable so a test can name
|
|
62
|
+
* the ids it expects
|
|
63
|
+
* @throws {TemplateEntryRejected} for the first entry the file describes unusably
|
|
64
|
+
*/
|
|
65
|
+
export function applyTemplateEntries(deps: {
|
|
66
|
+
readonly document: ProjectTemplateV1;
|
|
67
|
+
readonly placer: BlockPlacer;
|
|
68
|
+
readonly projectHelper: ProjectHelper;
|
|
69
|
+
readonly entries: ReadonlyMap<string, PreparedTemplateEntry>;
|
|
70
|
+
readonly newBlockId?: () => string;
|
|
71
|
+
}): AppliedEntry[] {
|
|
72
|
+
const { document, placer, projectHelper, entries } = deps;
|
|
73
|
+
const newBlockId = deps.newBlockId ?? randomUUID;
|
|
74
|
+
const added: AppliedEntry[] = [];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Template-local entry id → the block id it was given.
|
|
78
|
+
*
|
|
79
|
+
* An entry lands in here the moment it is given an id, before its block is placed. That is
|
|
80
|
+
* safe precisely because the apply is all-or-nothing: there is no surviving project in
|
|
81
|
+
* which a later entry could point at a block that never got created. It does mean an entry
|
|
82
|
+
* whose params reference itself is wired to itself rather than left dangling — a document
|
|
83
|
+
* the reference check rejects before an apply begins.
|
|
84
|
+
*/
|
|
85
|
+
const blockIds = new Map<string, string>();
|
|
86
|
+
|
|
87
|
+
for (const entry of document.blocks) {
|
|
88
|
+
const prepared = entries.get(entry.id);
|
|
89
|
+
if (prepared === undefined) {
|
|
90
|
+
throw new TemplateEntryRejected(entry.id, `No block was prepared for entry '${entry.id}'.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const blockConfig = extractConfig(prepared.blockPack.config);
|
|
94
|
+
|
|
95
|
+
// Only a block on the current storage facade can be told what to initialize itself
|
|
96
|
+
// with, and it is refused rather than created with its own defaults even when the
|
|
97
|
+
// entry carries no params: every entry names a kind, and a block predating the
|
|
98
|
+
// facade implements none, so creating one would honour the entry's pinned version
|
|
99
|
+
// while contradicting the kind it claims to be. With params it is worse still — a
|
|
100
|
+
// block that looks applied but ignored everything the template said about it.
|
|
101
|
+
//
|
|
102
|
+
// Kind resolution never picks such a block, since declaring a kind requires a
|
|
103
|
+
// recent SDK. An entry pinning an exact version can name anything ever published,
|
|
104
|
+
// which is the way this is reached.
|
|
105
|
+
if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) {
|
|
106
|
+
throw new TemplateEntryRejected(
|
|
107
|
+
entry.id,
|
|
108
|
+
"This version of the block is too old to be created from a template. Use a " +
|
|
109
|
+
"newer version of the block, or remove the pinned block version from this " +
|
|
110
|
+
"entry so a supported one is chosen automatically.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const blockId = newBlockId();
|
|
115
|
+
blockIds.set(entry.id, blockId);
|
|
116
|
+
|
|
117
|
+
// Params travelled from the file untouched, so pointing their references at this project
|
|
118
|
+
// is the block's own job, in its own bundle — nothing here knows which of the values in
|
|
119
|
+
// there carry block ids. It happens inside this same call, along with the kind's check and
|
|
120
|
+
// the block's own init: the block decides what its params mean, and this is the one place
|
|
121
|
+
// that asks it. The map holds the entries created so far, including this one, so an entry
|
|
122
|
+
// referencing itself is wired to itself and one referencing an entry below is left
|
|
123
|
+
// pointing at nothing.
|
|
124
|
+
//
|
|
125
|
+
// Params are a mapping by the time a document exists — the parser reads an omitted key
|
|
126
|
+
// as `{}` — so every entry goes through this path and is checked against its kind. Run
|
|
127
|
+
// before anything is placed, so params the block declines cost nothing but the report.
|
|
128
|
+
const storage = projectHelper.getInitialStorageFromParamsInVM(
|
|
129
|
+
blockConfig,
|
|
130
|
+
entry.params,
|
|
131
|
+
blockIds,
|
|
132
|
+
);
|
|
133
|
+
if (storage.error !== undefined) {
|
|
134
|
+
throw new TemplateEntryRejected(entry.id, storage.error.message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
placer.addBlock(
|
|
138
|
+
{ id: blockId, label: prepared.label, renderingMode: blockConfig.renderingMode },
|
|
139
|
+
{
|
|
140
|
+
storageMode: "fromModel",
|
|
141
|
+
blockPack: prepared.blockPack,
|
|
142
|
+
initialStorage: storage.value,
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
added.push({ templateLocalId: entry.id, blockId });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return added;
|
|
150
|
+
}
|