@platforma-sdk/model 1.81.0 → 1.82.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_migrations.cjs +92 -8
- package/dist/block_migrations.cjs.map +1 -1
- package/dist/block_migrations.d.ts +121 -32
- package/dist/block_migrations.d.ts.map +1 -1
- package/dist/block_migrations.js +92 -8
- package/dist/block_migrations.js.map +1 -1
- package/dist/block_model.cjs +69 -15
- package/dist/block_model.cjs.map +1 -1
- package/dist/block_model.d.ts +58 -22
- package/dist/block_model.d.ts.map +1 -1
- package/dist/block_model.js +71 -17
- package/dist/block_model.js.map +1 -1
- package/dist/block_storage_callbacks.cjs +194 -13
- package/dist/block_storage_callbacks.cjs.map +1 -1
- package/dist/block_storage_callbacks.js +192 -15
- package/dist/block_storage_callbacks.js.map +1 -1
- package/dist/block_storage_facade.cjs +4 -1
- package/dist/block_storage_facade.cjs.map +1 -1
- package/dist/block_storage_facade.d.ts +102 -0
- package/dist/block_storage_facade.d.ts.map +1 -1
- package/dist/block_storage_facade.js +4 -1
- package/dist/block_storage_facade.js.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.cjs +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.js +1 -1
- package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.js +1 -1
- package/package.json +9 -8
- package/src/block_migrations.ts +205 -55
- package/src/block_model.ts +190 -59
- package/src/block_storage_callbacks.ts +294 -15
- package/src/block_storage_facade.ts +95 -0
- package/src/components/PlDataTable/createPlDataTable/utils.test.ts +31 -0
- package/src/components/PlDataTable/createPlDataTable/utils.ts +4 -1
- package/src/kind_reference.test.ts +134 -0
- package/src/template_init.test.ts +413 -0
- package/src/template_params.test.ts +135 -0
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
} from "./block_storage";
|
|
26
26
|
import type { PluginHandle } from "./plugin_handle";
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
stringifyJson,
|
|
30
|
+
expandTemplateRefs,
|
|
31
|
+
relocateBlockIds,
|
|
32
|
+
type StringifiedJson,
|
|
33
|
+
} from "@milaboratories/pl-model-common";
|
|
29
34
|
import type { DataVersioned, TransferRecord } from "./block_migrations";
|
|
30
35
|
import type { StorageDebugView } from "@milaboratories/pl-model-middle-layer";
|
|
31
36
|
|
|
@@ -237,7 +242,20 @@ export function migrateStorage(
|
|
|
237
242
|
* @throws If initialDataFn or createPluginData throws
|
|
238
243
|
*/
|
|
239
244
|
export function createInitialStorage(hooks: InitialStorageHooks): StringifiedJson<BlockStorage> {
|
|
240
|
-
|
|
245
|
+
return assembleStorage(hooks.getDefaultBlockData(), hooks);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Wraps freshly created block data and freshly created plugin data into storage.
|
|
250
|
+
*
|
|
251
|
+
* Shared by the two ways a block's first storage comes into being — from defaults
|
|
252
|
+
* and from template params. Only the block's own data differs between them:
|
|
253
|
+
* plugins have no params channel, so they are always created at their defaults.
|
|
254
|
+
*/
|
|
255
|
+
function assembleStorage(
|
|
256
|
+
blockData: DataVersioned<unknown>,
|
|
257
|
+
hooks: Omit<InitialStorageHooks, "getDefaultBlockData">,
|
|
258
|
+
): StringifiedJson<BlockStorage> {
|
|
241
259
|
const pluginRegistry = hooks.getPluginRegistry();
|
|
242
260
|
|
|
243
261
|
const plugins: Record<PluginHandle, VersionedData<unknown>> = {};
|
|
@@ -248,14 +266,238 @@ export function createInitialStorage(hooks: InitialStorageHooks): StringifiedJso
|
|
|
248
266
|
|
|
249
267
|
const storage: BlockStorage = {
|
|
250
268
|
[BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
|
|
251
|
-
__dataVersion:
|
|
252
|
-
__data:
|
|
269
|
+
__dataVersion: blockData.version,
|
|
270
|
+
__data: blockData.data,
|
|
253
271
|
__pluginRegistry: pluginRegistry,
|
|
254
272
|
__plugins: plugins,
|
|
255
273
|
};
|
|
256
274
|
return stringifyJson(storage);
|
|
257
275
|
}
|
|
258
276
|
|
|
277
|
+
/** Dependencies for creating storage from a template entry's params. */
|
|
278
|
+
export interface ParamsStorageHooks extends Omit<InitialStorageHooks, "getDefaultBlockData"> {
|
|
279
|
+
/** The block's init factory, called with the entry's params. */
|
|
280
|
+
getBlockDataFromParams: (params: unknown) => DataVersioned<unknown>;
|
|
281
|
+
/**
|
|
282
|
+
* The kind's runtime params check. Applied before the factory sees anything, and its
|
|
283
|
+
* output is what the factory gets.
|
|
284
|
+
*/
|
|
285
|
+
parseInitializationParams: (value: unknown) => unknown;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Result of checking params against their kind: the params to use, or why they lost. */
|
|
289
|
+
export type TemplateParamsValidationResult =
|
|
290
|
+
| { error: string }
|
|
291
|
+
| { error?: undefined; value: unknown };
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check params against their kind's declared shape.
|
|
295
|
+
*
|
|
296
|
+
* The kind owns this rather than the block because the params contract belongs to the
|
|
297
|
+
* kind: many block versions implement one kind, and a per-block check would let them
|
|
298
|
+
* drift from each other and from the type.
|
|
299
|
+
*
|
|
300
|
+
* A parser rejects by throwing and accepts by returning the params to use, so its
|
|
301
|
+
* output — not the input — is what flows onward. That is what lets a kind strip keys
|
|
302
|
+
* it does not declare, which is the difference between a typo being ignored and a typo
|
|
303
|
+
* being reported.
|
|
304
|
+
*
|
|
305
|
+
* Every kind declares a parser, so every set of params that reaches here is checked;
|
|
306
|
+
* there is no pass-through path.
|
|
307
|
+
*
|
|
308
|
+
* @param value The params to check, references already in live form
|
|
309
|
+
* @param parseInitializationParams The kind's parser
|
|
310
|
+
*/
|
|
311
|
+
export function validateTemplateParams(
|
|
312
|
+
value: unknown,
|
|
313
|
+
parseInitializationParams: (value: unknown) => unknown,
|
|
314
|
+
): TemplateParamsValidationResult {
|
|
315
|
+
try {
|
|
316
|
+
return { value: parseInitializationParams(value) };
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// A rejection is an expected outcome for a hand-written file, so it is reported,
|
|
319
|
+
// not thrown.
|
|
320
|
+
return { error: `params do not match this block's kind: ${describeRejection(e)}` };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** One `{ path, message }` entry of a schema library's error. */
|
|
325
|
+
type IssueLike = { path?: unknown; message?: unknown };
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Render whatever a parser threw as one readable line.
|
|
329
|
+
*
|
|
330
|
+
* An error carrying an `issues` array is unpacked rather than printed: that is the
|
|
331
|
+
* shape zod (and several others) use, and its `message` is the whole issue list as
|
|
332
|
+
* JSON — technically complete and unreadable in a dialog. Duck-typed on purpose, since
|
|
333
|
+
* this package prescribes no schema library and takes no dependency on one; anything
|
|
334
|
+
* else falls back to its own message.
|
|
335
|
+
*/
|
|
336
|
+
function describeRejection(e: unknown): string {
|
|
337
|
+
const issues = (e as { issues?: unknown }).issues;
|
|
338
|
+
if (!Array.isArray(issues) || issues.length === 0) return messageOf(e);
|
|
339
|
+
|
|
340
|
+
return issues
|
|
341
|
+
.map((issue) => {
|
|
342
|
+
const { path, message } = issue as IssueLike;
|
|
343
|
+
const what = typeof message === "string" ? message : "is invalid";
|
|
344
|
+
const where = Array.isArray(path) ? formatPath(path) : "";
|
|
345
|
+
return where === "" ? what : `${where}: ${what}`;
|
|
346
|
+
})
|
|
347
|
+
.join("; ");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** `["numbers", 0]` → `numbers[0]` — how the params are written, not how they parse. */
|
|
351
|
+
function formatPath(path: readonly unknown[]): string {
|
|
352
|
+
return path.reduce<string>((acc, segment) => {
|
|
353
|
+
if (typeof segment === "number") return `${acc}[${segment}]`;
|
|
354
|
+
return acc === "" ? String(segment) : `${acc}.${String(segment)}`;
|
|
355
|
+
}, "");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Result of the `__pl_params_validate` callback.
|
|
360
|
+
*
|
|
361
|
+
* Carries no params back. The check is a pre-flight — its answer is "may this entry be
|
|
362
|
+
* applied", and the params that actually reach the block are produced by
|
|
363
|
+
* {@link createInitialStorageFromParams}, which parses again. One authoritative
|
|
364
|
+
* producer, rather than two values that could differ.
|
|
365
|
+
*/
|
|
366
|
+
export type InitializationParamsValidateCallbackResult = { error: string } | { error?: undefined };
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Check params that crossed into the model VM as text.
|
|
370
|
+
*
|
|
371
|
+
* The pre-flight entry point: a caller asks this before creating anything, once per
|
|
372
|
+
* template entry, so params a kind rejects are reported while there is still no
|
|
373
|
+
* project to half-build.
|
|
374
|
+
*
|
|
375
|
+
* The readable reference spelling is expanded here too, so what the kind checks is the shape it
|
|
376
|
+
* declared. The ids it sees are the file's own — no block exists yet — which is why a kind must
|
|
377
|
+
* not read meaning into a specific id.
|
|
378
|
+
*
|
|
379
|
+
* @param paramsJson The entry's params as JSON string
|
|
380
|
+
* @param parseInitializationParams The kind's parser
|
|
381
|
+
*/
|
|
382
|
+
export function validateTemplateParamsJson(
|
|
383
|
+
paramsJson: string,
|
|
384
|
+
parseInitializationParams: (value: unknown) => unknown,
|
|
385
|
+
): InitializationParamsValidateCallbackResult {
|
|
386
|
+
let params: unknown;
|
|
387
|
+
try {
|
|
388
|
+
params = JSON.parse(paramsJson);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
return { error: `params are not valid JSON: ${messageOf(e)}` };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Expanded before the kind sees anything, or a hand-written file would be rejected by every
|
|
394
|
+
// kind that declares a reference: `{ block, name }` is not a `PlRef` and no contract accepts
|
|
395
|
+
// one. Expansion needs nothing but the params, so this stays a two-argument check — the ids
|
|
396
|
+
// are not known yet and are not needed to answer the question this asks.
|
|
397
|
+
const result = validateTemplateParams(expandTemplateRefs(params), parseInitializationParams);
|
|
398
|
+
if (result.error !== undefined) return { error: result.error };
|
|
399
|
+
return {};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function messageOf(e: unknown): string {
|
|
403
|
+
return e instanceof Error ? e.message : String(e);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Result of building initial storage from params.
|
|
408
|
+
* Returned by the `__pl_storage_initialFromParams` callback.
|
|
409
|
+
*/
|
|
410
|
+
export type ParamsStorageResult =
|
|
411
|
+
| { error: string }
|
|
412
|
+
| { error?: undefined; storageJson: StringifiedJson<BlockStorage> };
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Creates complete initial storage for a block being created from template params.
|
|
416
|
+
*
|
|
417
|
+
* The inverse of {@link deriveTemplateParamsFromStorage}: that projects storage
|
|
418
|
+
* into params, this builds storage from them. The params are handed to the block's
|
|
419
|
+
* init factory, whose output is versioned and wrapped exactly as
|
|
420
|
+
* {@link createInitialStorage} wraps the defaults — so a block created from a
|
|
421
|
+
* template is indistinguishable from one created in the UI and then edited.
|
|
422
|
+
*
|
|
423
|
+
* **This is where a template's references become the block's own**, and it is the only place in
|
|
424
|
+
* a template's life where a reference is recognized at all. Params travel from the file
|
|
425
|
+
* untouched — the engine carrying them neither marks a reference nor reads one, because
|
|
426
|
+
* recognizing one means knowing the reference system, and that knowledge is here.
|
|
427
|
+
*
|
|
428
|
+
* Two things happen, in this order. The readable spelling a person may have written
|
|
429
|
+
* (`{ block, name }`) is expanded into the `PlRef` it stands for. Then every reference naming
|
|
430
|
+
* an entry that has a block is repointed at it: `blockIds` maps each template-local entry id to
|
|
431
|
+
* the block id it was given, and an id it does not name is left alone, which is how a reference
|
|
432
|
+
* to an entry created later ends up naming nothing rather than naming the wrong block.
|
|
433
|
+
*
|
|
434
|
+
* Relocation happens before the kind's parser and before the factory, so both see the ids the
|
|
435
|
+
* block will actually hold. It is one step of this function rather than a callback of its own
|
|
436
|
+
* precisely because nothing else wants its result: every VM call re-instantiates the runtime
|
|
437
|
+
* and re-evaluates the whole model bundle, so a separate call would parse the block twice per
|
|
438
|
+
* entry to produce a value only the next line reads.
|
|
439
|
+
*
|
|
440
|
+
* Params arrive as JSON text because this runs across the model-VM boundary, where
|
|
441
|
+
* only strings pass. Anything the factory rejects is returned as an error rather
|
|
442
|
+
* than thrown: applying a hand-written template is expected to surface bad params,
|
|
443
|
+
* and the applier reports every entry's problem in one pass.
|
|
444
|
+
*
|
|
445
|
+
* @param paramsJson - The entry's params as JSON string, exactly as the file held them
|
|
446
|
+
* @param blockIdsJson - template-local entry id → assigned block id, as a JSON object
|
|
447
|
+
* @param hooks - The block's init factory plus plugin creation
|
|
448
|
+
* @returns The storage to write, or why the params could not produce any
|
|
449
|
+
*/
|
|
450
|
+
export function createInitialStorageFromParams(
|
|
451
|
+
paramsJson: string,
|
|
452
|
+
blockIdsJson: string,
|
|
453
|
+
hooks: ParamsStorageHooks,
|
|
454
|
+
): ParamsStorageResult {
|
|
455
|
+
let params: unknown;
|
|
456
|
+
try {
|
|
457
|
+
params = JSON.parse(paramsJson);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
return { error: `params are not valid JSON: ${messageOf(e)}` };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Read separately from the params, and reported separately, because the two fail for
|
|
463
|
+
// completely different reasons. Params are the file's and a bad one is the author's mistake;
|
|
464
|
+
// this argument is the caller's, so the only way it arrives unreadable is a caller that does
|
|
465
|
+
// not send it — a middle layer older than this block, which is a build to refresh rather than
|
|
466
|
+
// anything to fix in the template. Folding both into one message sent the reader to the file.
|
|
467
|
+
let blockIds: Map<string, string>;
|
|
468
|
+
try {
|
|
469
|
+
blockIds = new Map(Object.entries(JSON.parse(blockIdsJson) as Record<string, string>));
|
|
470
|
+
} catch (e) {
|
|
471
|
+
return {
|
|
472
|
+
error:
|
|
473
|
+
`this block was not told which blocks the template's references should point at ` +
|
|
474
|
+
`(${messageOf(e)}). The application applying the template is older than the block; ` +
|
|
475
|
+
`rebuild or update it.`,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
// Spelling first, ids second. Expansion turns `{ block, name }` into a `PlRef` naming an
|
|
481
|
+
// ENTRY, which is exactly what relocation then repoints — so both spellings reach the
|
|
482
|
+
// block through the same step and cannot diverge in how they behave.
|
|
483
|
+
params = relocateBlockIds(expandTemplateRefs(params), blockIds);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
return { error: `this entry's references could not be relocated: ${messageOf(e)}` };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Checked here too, not only in the caller's pre-flight pass. The pre-flight is
|
|
489
|
+
// about reporting every bad entry before anything is created; this is about the
|
|
490
|
+
// factory never being handed a value the kind rejects, whichever path got here.
|
|
491
|
+
const checked = validateTemplateParams(params, hooks.parseInitializationParams);
|
|
492
|
+
if (checked.error !== undefined) return { error: checked.error };
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
return { storageJson: assembleStorage(hooks.getBlockDataFromParams(checked.value), hooks) };
|
|
496
|
+
} catch (e) {
|
|
497
|
+
return { error: `init() threw on the given params: ${messageOf(e)}` };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
259
501
|
// =============================================================================
|
|
260
502
|
// Args Derivation from Storage
|
|
261
503
|
// =============================================================================
|
|
@@ -271,19 +513,19 @@ export type ArgsDeriveResult = { error: string } | { error?: undefined; value: u
|
|
|
271
513
|
* This extracts data from storage and passes it to the block's args() function.
|
|
272
514
|
*
|
|
273
515
|
* @param storageJson - Storage as JSON string
|
|
274
|
-
* @param
|
|
516
|
+
* @param deriveArgs - The block's args derivation function
|
|
275
517
|
* @returns ArgsDeriveResult with derived args or error
|
|
276
518
|
*/
|
|
277
519
|
export function deriveArgsFromStorage(
|
|
278
520
|
storageJson: string,
|
|
279
|
-
|
|
521
|
+
deriveArgs: (data: unknown) => unknown,
|
|
280
522
|
): ArgsDeriveResult {
|
|
281
523
|
// Extract data from storage
|
|
282
524
|
const { data } = normalizeStorage(storageJson);
|
|
283
525
|
|
|
284
526
|
// Call the args function with extracted data
|
|
285
527
|
try {
|
|
286
|
-
const result =
|
|
528
|
+
const result = deriveArgs(data);
|
|
287
529
|
return { value: result };
|
|
288
530
|
} catch (e) {
|
|
289
531
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
@@ -293,25 +535,25 @@ export function deriveArgsFromStorage(
|
|
|
293
535
|
|
|
294
536
|
/**
|
|
295
537
|
* Derives prerunArgs from storage.
|
|
296
|
-
* Uses
|
|
538
|
+
* Uses derivePrerunArgs if provided, otherwise falls back to deriveArgs.
|
|
297
539
|
*
|
|
298
540
|
* @param storageJson - Storage as JSON string
|
|
299
|
-
* @param
|
|
300
|
-
* @param
|
|
541
|
+
* @param deriveArgs - The block's args derivation function (fallback)
|
|
542
|
+
* @param derivePrerunArgs - Optional prerun args derivation function
|
|
301
543
|
* @returns ArgsDeriveResult with derived prerunArgs or error
|
|
302
544
|
*/
|
|
303
545
|
export function derivePrerunArgsFromStorage(
|
|
304
546
|
storageJson: string,
|
|
305
|
-
|
|
306
|
-
|
|
547
|
+
deriveArgs: (data: unknown) => unknown,
|
|
548
|
+
derivePrerunArgs?: (data: unknown) => unknown,
|
|
307
549
|
): ArgsDeriveResult {
|
|
308
550
|
// Extract data from storage
|
|
309
551
|
const { data } = normalizeStorage(storageJson);
|
|
310
552
|
|
|
311
553
|
// Try prerunArgs function first if available
|
|
312
|
-
if (
|
|
554
|
+
if (derivePrerunArgs) {
|
|
313
555
|
try {
|
|
314
|
-
const result =
|
|
556
|
+
const result = derivePrerunArgs(data);
|
|
315
557
|
return { value: result };
|
|
316
558
|
} catch (e) {
|
|
317
559
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
@@ -321,7 +563,7 @@ export function derivePrerunArgsFromStorage(
|
|
|
321
563
|
|
|
322
564
|
// Fall back to args function
|
|
323
565
|
try {
|
|
324
|
-
const result =
|
|
566
|
+
const result = deriveArgs(data);
|
|
325
567
|
return { value: result };
|
|
326
568
|
} catch (e) {
|
|
327
569
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
@@ -329,5 +571,42 @@ export function derivePrerunArgsFromStorage(
|
|
|
329
571
|
}
|
|
330
572
|
}
|
|
331
573
|
|
|
574
|
+
// =============================================================================
|
|
575
|
+
// Template Entry Derivation from Storage
|
|
576
|
+
// =============================================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Derives this block's template-entry params from storage.
|
|
580
|
+
*
|
|
581
|
+
* The inverse of the data model's `init`: `init` turns `params` into data, this
|
|
582
|
+
* turns data back into the params that would recreate it.
|
|
583
|
+
*
|
|
584
|
+
* The lambda returns ordinary live params — references as `PlRef`s, column identifiers as they
|
|
585
|
+
* are stored — and that is exactly what gets written. Nothing is marked, normalized or
|
|
586
|
+
* rewritten on the way out: a template holds what the block holds. Repointing those references
|
|
587
|
+
* at another project is the business of {@link createInitialStorageFromParams}, on the way back
|
|
588
|
+
* in, where the ids to point at are known.
|
|
589
|
+
*
|
|
590
|
+
* Every block declares the lambda, so every export produces params; a block with
|
|
591
|
+
* nothing worth restoring returns `{}` rather than declining.
|
|
592
|
+
*
|
|
593
|
+
* @param storageJson - Storage as JSON string
|
|
594
|
+
* @param deriveTemplateParams - The block's templateParams lambda
|
|
595
|
+
* @returns ArgsDeriveResult holding the params exactly as the block projected them
|
|
596
|
+
*/
|
|
597
|
+
export function deriveTemplateParamsFromStorage<TP extends (data: unknown) => unknown>(
|
|
598
|
+
storageJson: string,
|
|
599
|
+
deriveTemplateParams: TP,
|
|
600
|
+
): ArgsDeriveResult {
|
|
601
|
+
const { data } = normalizeStorage(storageJson);
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
return { value: deriveTemplateParams(data) };
|
|
605
|
+
} catch (e) {
|
|
606
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
607
|
+
return { error: `templateParams() threw: ${errorMsg}` };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
332
611
|
// Export discriminator key and schema version for external checks
|
|
333
612
|
export { BLOCK_STORAGE_KEY, BLOCK_STORAGE_SCHEMA_VERSION };
|
|
@@ -82,6 +82,9 @@ export const BlockStorageFacadeCallbacks = {
|
|
|
82
82
|
ArgsDerive: "__pl_args_derive",
|
|
83
83
|
PrerunArgsDerive: "__pl_prerunArgs_derive",
|
|
84
84
|
StorageInitial: "__pl_storage_initial",
|
|
85
|
+
InitializationParamsDerive: "__pl_initializationParams_derive",
|
|
86
|
+
StorageInitialFromParams: "__pl_storage_initialFromParams",
|
|
87
|
+
InitializationParamsValidate: "__pl_initializationParams_validate",
|
|
85
88
|
} as const;
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -189,6 +192,98 @@ export interface BlockStorageFacade {
|
|
|
189
192
|
* @returns Initial storage as JSON string
|
|
190
193
|
*/
|
|
191
194
|
[BlockStorageFacadeCallbacks.StorageInitial]: () => StringifiedJson;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Derive this block's template entry params from storage.
|
|
198
|
+
* Called when exporting the project as a template.
|
|
199
|
+
*
|
|
200
|
+
* Every V3 block declares `.templateParams()` — the model does not build without
|
|
201
|
+
* it — so every export produces params. A block whose state carries nothing worth
|
|
202
|
+
* restoring returns `{}`, which is written out and used as-is by init.
|
|
203
|
+
*
|
|
204
|
+
* The returned params are written out exactly as they come back, references included: the
|
|
205
|
+
* template engine carrying them recognizes nothing in there, and the block that receives
|
|
206
|
+
* them on the way back in is what repoints them. The caller supplies everything else in the
|
|
207
|
+
* entry — `id`, `kind` — so the lambda cannot set them.
|
|
208
|
+
*
|
|
209
|
+
* @param storageJson - Storage as JSON string
|
|
210
|
+
* @returns Either an error, or the params to write
|
|
211
|
+
*/
|
|
212
|
+
[BlockStorageFacadeCallbacks.InitializationParamsDerive]: (
|
|
213
|
+
storageJson: StringifiedJson,
|
|
214
|
+
) => { error: string } | { error?: undefined; value: unknown };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get initial storage JSON for a block created from params.
|
|
218
|
+
* Called when applying a template, once per entry that carries `params`.
|
|
219
|
+
*
|
|
220
|
+
* The mirror image of {@link BlockStorageFacadeCallbacks.InitializationParamsDerive}:
|
|
221
|
+
* that one turns storage into params, this one turns params into storage. Every
|
|
222
|
+
* applied entry comes through here, including one whose file omitted `params` —
|
|
223
|
+
* an omitted key is read as `{}` and checked like any other value, so no entry
|
|
224
|
+
* reaches a block without passing its kind's contract.
|
|
225
|
+
* {@link BlockStorageFacadeCallbacks.StorageInitial} stays the UI-creation path,
|
|
226
|
+
* where there are genuinely no params.
|
|
227
|
+
*
|
|
228
|
+
* Separate from `StorageInitial` rather than an optional argument to it,
|
|
229
|
+
* deliberately: a block bundled with an older SDK does not register this
|
|
230
|
+
* callback at all, so the caller sees it missing and can say so. Widening
|
|
231
|
+
* `StorageInitial` would instead have that block silently ignore the params and
|
|
232
|
+
* produce a default-initialized block that looks successfully applied.
|
|
233
|
+
*
|
|
234
|
+
* The params arrive as ordinary live params — references are `PlRef`s, already
|
|
235
|
+
* pointing at the ids the target project just assigned. Resolution happens in
|
|
236
|
+
* the engine, before this call, so a block's init factory never handles a
|
|
237
|
+
* template-local reference.
|
|
238
|
+
*
|
|
239
|
+
* Errors are returned, not thrown: a factory rejecting params it cannot use is
|
|
240
|
+
* an expected outcome for a hand-written template file, and every entry's
|
|
241
|
+
* problem is reported together.
|
|
242
|
+
*
|
|
243
|
+
* **Also where the entry's references are pointed at this project.** Params reach here
|
|
244
|
+
* exactly as the file held them: nothing between the block that exported them and this call
|
|
245
|
+
* marks a reference, reads one, or rewrites one, because recognizing one means knowing the
|
|
246
|
+
* reference system and this bundle is where that knowledge lives. `blockIdsJson` maps each
|
|
247
|
+
* template-local entry id to the block id it was given, holding the entries created so far —
|
|
248
|
+
* so an id it does not name is left alone, and a reference to an entry created later names
|
|
249
|
+
* nothing rather than naming the wrong block.
|
|
250
|
+
*
|
|
251
|
+
* Relocating and initializing are one call rather than two because every call
|
|
252
|
+
* re-instantiates the runtime and re-evaluates the whole model bundle: splitting them would
|
|
253
|
+
* parse the block twice per entry to produce an intermediate only the second half reads.
|
|
254
|
+
*
|
|
255
|
+
* @param paramsJson - The entry's params as JSON string, as the file held them
|
|
256
|
+
* @param blockIdsJson - template-local entry id → assigned block id, as a JSON object
|
|
257
|
+
* @returns Either an error, or the initial storage as JSON string
|
|
258
|
+
*/
|
|
259
|
+
[BlockStorageFacadeCallbacks.StorageInitialFromParams]: (
|
|
260
|
+
paramsJson: StringifiedJson,
|
|
261
|
+
blockIdsJson: StringifiedJson,
|
|
262
|
+
) => { error: string } | { error?: undefined; storageJson: StringifiedJson };
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check params against this block's kind, creating nothing.
|
|
266
|
+
*
|
|
267
|
+
* Called once per entry before a template is applied, so params a kind rejects are
|
|
268
|
+
* reported against the entry that carries them while there is still no project — the
|
|
269
|
+
* same reason references and ids are checked before construction starts. Applying
|
|
270
|
+
* without this call is safe but worse: `StorageInitialFromParams` runs the same check
|
|
271
|
+
* and refuses, by which point earlier entries have already been created.
|
|
272
|
+
*
|
|
273
|
+
* A pass carries nothing back but its own absence of error: every kind declares a
|
|
274
|
+
* parser, so a pass means the params were checked against the contract, not merely
|
|
275
|
+
* that they were valid JSON.
|
|
276
|
+
*
|
|
277
|
+
* Reference ids inside the params may be placeholders at this point: the check runs
|
|
278
|
+
* before blocks exist, so what is verified is the shape of the params, not what they
|
|
279
|
+
* point at.
|
|
280
|
+
*
|
|
281
|
+
* @param paramsJson The entry's params as JSON string
|
|
282
|
+
* @returns Why the params were rejected, or nothing
|
|
283
|
+
*/
|
|
284
|
+
[BlockStorageFacadeCallbacks.InitializationParamsValidate]: (
|
|
285
|
+
paramsJson: StringifiedJson,
|
|
286
|
+
) => { error: string } | { error?: undefined };
|
|
192
287
|
}
|
|
193
288
|
|
|
194
289
|
/** Register all facade callbacks at once. Ensures all required callbacks are provided. */
|
|
@@ -207,6 +207,37 @@ describe("evaluateRules", () => {
|
|
|
207
207
|
expect(result.get(gid("b"))?.priority).toBe(50);
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
test("a rule matching zero columns does not suppress the other rules", () => {
|
|
211
|
+
const rules: ColumnOrderRule[] = [
|
|
212
|
+
{ match: { name: "^nothing-matches-this$" }, priority: 999 },
|
|
213
|
+
{ match: { name: "^alpha$" }, priority: 100 },
|
|
214
|
+
{ match: { name: "^beta$" }, priority: 50 },
|
|
215
|
+
];
|
|
216
|
+
const columns = [makeLazyColumn("a", { name: "alpha" }), makeLazyColumn("b", { name: "beta" })];
|
|
217
|
+
|
|
218
|
+
const result = evaluateRules(rules, columns);
|
|
219
|
+
|
|
220
|
+
expect(result.get(gid("a"))?.priority).toBe(100);
|
|
221
|
+
expect(result.get(gid("b"))?.priority).toBe(50);
|
|
222
|
+
expect(result.size).toBe(2);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("rules matching zero columns are skipped regardless of their position", () => {
|
|
226
|
+
const rules: ColumnVisibilityRule[] = [
|
|
227
|
+
{ match: { name: "^ghost-a$" }, visibility: "hidden" },
|
|
228
|
+
{ match: { name: "^note$" }, visibility: "hidden" },
|
|
229
|
+
{ match: { name: "^ghost-b$" }, visibility: "hidden" },
|
|
230
|
+
{ match: { name: "^score$" }, visibility: "optional" },
|
|
231
|
+
{ match: { name: "^ghost-c$" }, visibility: "hidden" },
|
|
232
|
+
];
|
|
233
|
+
const columns = [makeLazyColumn("n", { name: "note" }), makeLazyColumn("s", { name: "score" })];
|
|
234
|
+
|
|
235
|
+
const result = evaluateRules(rules, columns);
|
|
236
|
+
|
|
237
|
+
expect(result.get(gid("n"))?.visibility).toBe("hidden");
|
|
238
|
+
expect(result.get(gid("s"))?.visibility).toBe("optional");
|
|
239
|
+
});
|
|
240
|
+
|
|
210
241
|
test("dedupes columns by id before building spec frame (no duplicate-key crash)", () => {
|
|
211
242
|
const rules: ColumnVisibilityRule[] = [{ match: { name: "^dup$" }, visibility: "hidden" }];
|
|
212
243
|
const dup = makeLazyColumn("d", { name: "dup" });
|
|
@@ -88,7 +88,10 @@ export function evaluateRules<R extends { match: ColumnSelector }>(
|
|
|
88
88
|
const baseCollection = ColumnsCollection([{ columns: baseColumns, isFinal: true }]);
|
|
89
89
|
for (const rule of selectorRules) {
|
|
90
90
|
const hitIds = baseCollection.filter({ include: rule.match }).getColumnIds();
|
|
91
|
-
|
|
91
|
+
// A rule matching nothing simply contributes no entries — it must not
|
|
92
|
+
// discard the rules evaluated alongside it. The lookup below is
|
|
93
|
+
// null-safe, so leaving this rule out of the map is enough.
|
|
94
|
+
if (hitIds.length === 0) continue;
|
|
92
95
|
selectorHitsByRule.set(rule, new Set(hitIds.map((id) => extractPObjectId(id))));
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
2
|
+
import type { BlockConfigContainer, BlockKindReference } from "@milaboratories/pl-model-common";
|
|
3
|
+
import {
|
|
4
|
+
PROJECT_TEMPLATE_SCHEMA_V1,
|
|
5
|
+
parseProjectTemplateV1,
|
|
6
|
+
kindReferenceToSelectorReference,
|
|
7
|
+
} from "@milaboratories/pl-model-common";
|
|
8
|
+
import { extractConfig } from "./bconfig/normalization";
|
|
9
|
+
import { BlockModelV3 } from "./block_model";
|
|
10
|
+
import { DataModelBuilder } from "./block_migrations";
|
|
11
|
+
import { defineBlockKind } from "@platforma-sdk/block-kind";
|
|
12
|
+
|
|
13
|
+
// Template export needs one fact to hold at runtime: the kind reference a block
|
|
14
|
+
// declared at build time is still readable from its config, and reaches a
|
|
15
|
+
// template entry unchanged.
|
|
16
|
+
//
|
|
17
|
+
// No new code is needed for that read — `BlockConfigContainer.kind` is already
|
|
18
|
+
// typed `BlockKindReference | undefined`, so the middle layer's read is the
|
|
19
|
+
// property access `bp.info.config.kind`, and the widen to an entry's selector
|
|
20
|
+
// form already exists. What did not exist is proof that the path holds end to
|
|
21
|
+
// end, which is what this suite is: bake -> read -> widen -> entry, inside one
|
|
22
|
+
// package, with no backend.
|
|
23
|
+
|
|
24
|
+
type Params = { label: string };
|
|
25
|
+
type BlockData = { label: string };
|
|
26
|
+
|
|
27
|
+
const KIND_NAME = "@platforma-open/milaboratories.demo.kind";
|
|
28
|
+
const KIND_VERSION = "1.4.2";
|
|
29
|
+
const KIND_REF = `${KIND_NAME}@${KIND_VERSION}`;
|
|
30
|
+
|
|
31
|
+
const kind = defineBlockKind<Params>({
|
|
32
|
+
name: KIND_NAME,
|
|
33
|
+
version: KIND_VERSION,
|
|
34
|
+
parseInitializationParams: (value) => value as Params,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const dataModel = new DataModelBuilder({ kind })
|
|
38
|
+
.from<BlockData>("v1")
|
|
39
|
+
.init(({ params }) => ({ label: params?.label ?? "" }));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The config container `build-model` serializes into model.json.
|
|
43
|
+
*
|
|
44
|
+
* `done()` returns it whenever the model is not running in a UI — `isInUI()`
|
|
45
|
+
* checks for a `platforma` global, absent under vitest — which is exactly the
|
|
46
|
+
* path the build takes.
|
|
47
|
+
*/
|
|
48
|
+
function containerOf(model: unknown): BlockConfigContainer {
|
|
49
|
+
return (model as { config: BlockConfigContainer }).config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const kindfulContainer = () =>
|
|
53
|
+
containerOf(
|
|
54
|
+
BlockModelV3.create({ dataModel, kind })
|
|
55
|
+
.args((data) => ({ label: data.label }))
|
|
56
|
+
.templateParams((data) => ({ label: data.label }))
|
|
57
|
+
.done(),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
describe("reading the kind reference back at runtime", () => {
|
|
61
|
+
test("done() bakes the declared kind at the container level", () => {
|
|
62
|
+
// Beside `code`, orthogonal to the render envelope — a future v5 envelope
|
|
63
|
+
// would not move it.
|
|
64
|
+
expect(kindfulContainer().kind).toBe(KIND_REF);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("the NORMALIZED config does not carry it — the container is the read point", () => {
|
|
68
|
+
// extractConfig normalizes the render envelope and returns only envelope
|
|
69
|
+
// fields, so the `cfg` every getBlockPackInfo caller holds is kind-blind by
|
|
70
|
+
// construction (lib/node/pl-middle-layer/src/middle_layer/util.ts). An
|
|
71
|
+
// exporter that goes looking there finds nothing and must read
|
|
72
|
+
// `info.config.kind` instead. Pinned so that stays true, or fails loudly.
|
|
73
|
+
expect("kind" in extractConfig(kindfulContainer())).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("the authoring API can no longer produce a kind-less block", () => {
|
|
77
|
+
const kindlessDataModel = new DataModelBuilder().from<BlockData>("v1").init(() => ({
|
|
78
|
+
label: "",
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// The kind-less `create(dataModel)` overload is gone: a kind is mandatory, so there
|
|
82
|
+
// is no longer a way to author a block whose container carries no kind reference.
|
|
83
|
+
// @ts-expect-error - create takes { dataModel, kind }; a bare DataModel is not it
|
|
84
|
+
expect(() => BlockModelV3.create(kindlessDataModel)).toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the READ side stays optional — already-published blocks carry no kind", () => {
|
|
88
|
+
// `BlockConfigContainer.kind` is `BlockKindReference | undefined` and must stay that
|
|
89
|
+
// way: every block published before kinds existed is in that state, and the middle
|
|
90
|
+
// layer reads those configs. What the exporter should DO with such a block is
|
|
91
|
+
// decided — a template entry's `kind` is required, so there is no legal entry to
|
|
92
|
+
// write and the export fails naming the block (`template_serializer.ts`).
|
|
93
|
+
const container = kindfulContainer();
|
|
94
|
+
|
|
95
|
+
expect(container.kind).toBe(KIND_REF);
|
|
96
|
+
expectTypeOf(container.kind).toEqualTypeOf<BlockKindReference | undefined>();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("the reference as a template entry's kind", () => {
|
|
101
|
+
test("it widens to the exact tier, string unchanged", () => {
|
|
102
|
+
// An entry's kind is the exact version the block implements, `{name}@X.Y.Z`,
|
|
103
|
+
// read from the model's embedded kind reference. Widening changes the brand,
|
|
104
|
+
// not the string — export never loosens to a `~` or `^` tier.
|
|
105
|
+
expect(kindReferenceToSelectorReference(kindfulContainer().kind!)).toBe(KIND_REF);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the org-scoped name survives the split", () => {
|
|
109
|
+
// The kind name itself starts with `@`, so splitting on the FIRST `@` would
|
|
110
|
+
// truncate it to the empty name. block_kind_ref.ts owns that rule by
|
|
111
|
+
// splitting on the last `@`; this pins that a reference a real block
|
|
112
|
+
// declares round-trips through it intact.
|
|
113
|
+
const widened = kindReferenceToSelectorReference(kindfulContainer().kind!);
|
|
114
|
+
expect(widened.startsWith("@platforma-open/")).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("the widened reference is accepted as a template-v1 entry's kind", () => {
|
|
118
|
+
const blockId = "3f1c2b7a-0000-4000-8000-000000000001";
|
|
119
|
+
|
|
120
|
+
const [entry] = parseProjectTemplateV1({
|
|
121
|
+
schema: PROJECT_TEMPLATE_SCHEMA_V1,
|
|
122
|
+
blocks: [{ id: blockId, kind: kindReferenceToSelectorReference(kindfulContainer().kind!) }],
|
|
123
|
+
}).blocks;
|
|
124
|
+
|
|
125
|
+
// The whole path, end to end: what the block declared is what the file
|
|
126
|
+
// carries. `id` is the block's project-local UUID, reused verbatim, and `params` is the
|
|
127
|
+
// empty mapping the parser settles an omitted key to.
|
|
128
|
+
expect(entry).toEqual({ id: blockId, kind: KIND_REF, params: {} });
|
|
129
|
+
|
|
130
|
+
// No `block` override: a block implements exactly one kind version, so
|
|
131
|
+
// export has nothing to pin.
|
|
132
|
+
expect(entry.block).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|