@platforma-sdk/model 1.81.1 → 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/package.cjs +1 -1
- package/dist/package.js +1 -1
- package/package.json +10 -9
- 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/kind_reference.test.ts +134 -0
- package/src/template_init.test.ts +413 -0
- package/src/template_params.test.ts +135 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_block_storage = require("./block_storage.cjs");
|
|
2
|
+
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
2
3
|
//#region src/block_migrations.ts
|
|
3
4
|
/** Create a DataVersioned wrapper with correct shape */
|
|
4
5
|
function makeVersionedData(version, data) {
|
|
@@ -47,10 +48,16 @@ var MigrationChainBase = class {
|
|
|
47
48
|
versionChain;
|
|
48
49
|
migrationSteps;
|
|
49
50
|
transferSteps;
|
|
51
|
+
/** Kind reference seeded by the builder, threaded through the chain into init(). */
|
|
52
|
+
kindRef;
|
|
53
|
+
/** The kind's runtime params check, carried for `init()` to hand to the DataModel. */
|
|
54
|
+
parseInitializationParams;
|
|
50
55
|
constructor(state) {
|
|
51
56
|
this.versionChain = state.versionChain;
|
|
52
57
|
this.migrationSteps = state.steps;
|
|
53
58
|
this.transferSteps = state.transferSteps ?? [];
|
|
59
|
+
this.kindRef = state.kindRef;
|
|
60
|
+
this.parseInitializationParams = state.parseInitializationParams;
|
|
54
61
|
}
|
|
55
62
|
/** Appends a migration step and returns the new versionChain and steps arrays. */
|
|
56
63
|
buildStep(nextVersion, fn) {
|
|
@@ -92,6 +99,8 @@ var MigrationChainBase = class {
|
|
|
92
99
|
steps: this.migrationSteps,
|
|
93
100
|
transferSteps: this.transferSteps,
|
|
94
101
|
initialDataFn: initialData,
|
|
102
|
+
kindRef: this.kindRef,
|
|
103
|
+
parseInitializationParams: this.parseInitializationParams,
|
|
95
104
|
...this.recoverState()
|
|
96
105
|
});
|
|
97
106
|
}
|
|
@@ -130,6 +139,8 @@ var DataModelMigrationChainWithRecover = class DataModelMigrationChainWithRecove
|
|
|
130
139
|
versionChain,
|
|
131
140
|
steps,
|
|
132
141
|
transferSteps: this.transferSteps,
|
|
142
|
+
kindRef: this.kindRef,
|
|
143
|
+
parseInitializationParams: this.parseInitializationParams,
|
|
133
144
|
recoverFn: this.recoverFn,
|
|
134
145
|
recoverFromIndex: this.recoverFromIndex
|
|
135
146
|
});
|
|
@@ -145,6 +156,8 @@ var DataModelMigrationChainWithRecover = class DataModelMigrationChainWithRecove
|
|
|
145
156
|
versionChain: this.versionChain,
|
|
146
157
|
steps: this.migrationSteps,
|
|
147
158
|
transferSteps,
|
|
159
|
+
kindRef: this.kindRef,
|
|
160
|
+
parseInitializationParams: this.parseInitializationParams,
|
|
148
161
|
recoverFn: this.recoverFn,
|
|
149
162
|
recoverFromIndex: this.recoverFromIndex
|
|
150
163
|
});
|
|
@@ -162,11 +175,13 @@ var DataModelMigrationChainWithRecover = class DataModelMigrationChainWithRecove
|
|
|
162
175
|
*/
|
|
163
176
|
var DataModelMigrationChain = class DataModelMigrationChain extends MigrationChainBase {
|
|
164
177
|
/** @internal */
|
|
165
|
-
constructor({ versionChain, steps = [], transferSteps = [] }) {
|
|
178
|
+
constructor({ versionChain, steps = [], transferSteps = [], kindRef, parseInitializationParams }) {
|
|
166
179
|
super({
|
|
167
180
|
versionChain,
|
|
168
181
|
steps,
|
|
169
|
-
transferSteps
|
|
182
|
+
transferSteps,
|
|
183
|
+
kindRef,
|
|
184
|
+
parseInitializationParams
|
|
170
185
|
});
|
|
171
186
|
}
|
|
172
187
|
/**
|
|
@@ -185,7 +200,9 @@ var DataModelMigrationChain = class DataModelMigrationChain extends MigrationCha
|
|
|
185
200
|
return new DataModelMigrationChain({
|
|
186
201
|
versionChain,
|
|
187
202
|
steps,
|
|
188
|
-
transferSteps: this.transferSteps
|
|
203
|
+
transferSteps: this.transferSteps,
|
|
204
|
+
kindRef: this.kindRef,
|
|
205
|
+
parseInitializationParams: this.parseInitializationParams
|
|
189
206
|
});
|
|
190
207
|
}
|
|
191
208
|
/**
|
|
@@ -206,7 +223,9 @@ var DataModelMigrationChain = class DataModelMigrationChain extends MigrationCha
|
|
|
206
223
|
return new DataModelMigrationChain({
|
|
207
224
|
versionChain: this.versionChain,
|
|
208
225
|
steps: this.migrationSteps,
|
|
209
|
-
transferSteps
|
|
226
|
+
transferSteps,
|
|
227
|
+
kindRef: this.kindRef,
|
|
228
|
+
parseInitializationParams: this.parseInitializationParams
|
|
210
229
|
});
|
|
211
230
|
}
|
|
212
231
|
/**
|
|
@@ -237,6 +256,8 @@ var DataModelMigrationChain = class DataModelMigrationChain extends MigrationCha
|
|
|
237
256
|
versionChain: this.versionChain,
|
|
238
257
|
steps: this.migrationSteps,
|
|
239
258
|
transferSteps: this.transferSteps,
|
|
259
|
+
kindRef: this.kindRef,
|
|
260
|
+
parseInitializationParams: this.parseInitializationParams,
|
|
240
261
|
recoverFn: fn,
|
|
241
262
|
recoverFromIndex: this.migrationSteps.length
|
|
242
263
|
});
|
|
@@ -299,6 +320,8 @@ var DataModelInitialChain = class extends DataModelMigrationChain {
|
|
|
299
320
|
return new DataModelMigrationChainWithRecover({
|
|
300
321
|
versionChain: [require_block_storage.DATA_MODEL_LEGACY_VERSION, ...this.versionChain],
|
|
301
322
|
steps: [step, ...this.migrationSteps],
|
|
323
|
+
kindRef: this.kindRef,
|
|
324
|
+
parseInitializationParams: this.parseInitializationParams,
|
|
302
325
|
transferSteps: this.transferSteps.map((t) => ({
|
|
303
326
|
...t,
|
|
304
327
|
beforeStepIndex: t.beforeStepIndex + 1
|
|
@@ -353,6 +376,21 @@ var DataModelInitialChain = class extends DataModelMigrationChain {
|
|
|
353
376
|
* .init(() => ({ inputFile: '', selectedTab: 'main' }));
|
|
354
377
|
*/
|
|
355
378
|
var DataModelBuilder = class {
|
|
379
|
+
#kindRef;
|
|
380
|
+
#parseInitializationParams;
|
|
381
|
+
/**
|
|
382
|
+
* @param opts.kind - The block kind this data model implements. Its reference
|
|
383
|
+
* is captured and baked into the config so the manifest can advertise which
|
|
384
|
+
* kind the block satisfies, and its `Params` type flows into `.init()`.
|
|
385
|
+
* Optional during the transition window while existing V3 blocks are
|
|
386
|
+
* migrated to kind-carrying builders; a kind-less builder simply carries no
|
|
387
|
+
* reference and the reconciler can't project it yet. Object form mirrors
|
|
388
|
+
* `BlockModelV3.create({ dataModel, kind })`.
|
|
389
|
+
*/
|
|
390
|
+
constructor(opts) {
|
|
391
|
+
this.#kindRef = opts?.kind ? (0, _milaboratories_pl_model_common.formatKindRef)(opts.kind) : void 0;
|
|
392
|
+
this.#parseInitializationParams = opts?.kind?.parseInitializationParams;
|
|
393
|
+
}
|
|
356
394
|
/**
|
|
357
395
|
* Start the migration chain with the given initial data type and version key.
|
|
358
396
|
*
|
|
@@ -361,7 +399,11 @@ var DataModelBuilder = class {
|
|
|
361
399
|
* @returns Migration chain builder
|
|
362
400
|
*/
|
|
363
401
|
from(initialVersion) {
|
|
364
|
-
return new DataModelInitialChain({
|
|
402
|
+
return new DataModelInitialChain({
|
|
403
|
+
versionChain: [initialVersion],
|
|
404
|
+
kindRef: this.#kindRef,
|
|
405
|
+
parseInitializationParams: this.#parseInitializationParams
|
|
406
|
+
});
|
|
365
407
|
}
|
|
366
408
|
};
|
|
367
409
|
/**
|
|
@@ -393,13 +435,19 @@ var DataModel = class DataModel {
|
|
|
393
435
|
initialDataFn;
|
|
394
436
|
recoverFn;
|
|
395
437
|
recoverFromIndex;
|
|
396
|
-
|
|
438
|
+
/** Reference to the block kind this data model was built for, if any. */
|
|
439
|
+
_kindRef;
|
|
440
|
+
/** The kind's runtime params check, if it declares one. */
|
|
441
|
+
_parseInitializationParams;
|
|
442
|
+
constructor({ versionChain, steps, transferSteps = [], initialDataFn, kindRef, parseInitializationParams, recoverFn = defaultRecover, recoverFromIndex }) {
|
|
397
443
|
if (versionChain.length === 0) throw new Error("DataModel requires at least one version key");
|
|
398
444
|
this.latestVersion = versionChain[versionChain.length - 1];
|
|
399
445
|
this.stepsByFromVersion = new Map(versionChain.map((v, i) => [v, i]));
|
|
400
446
|
this.steps = steps;
|
|
401
447
|
this.transferSteps = transferSteps;
|
|
402
448
|
this.initialDataFn = initialDataFn;
|
|
449
|
+
this._kindRef = kindRef;
|
|
450
|
+
this._parseInitializationParams = parseInitializationParams;
|
|
403
451
|
this.recoverFn = recoverFn;
|
|
404
452
|
this.recoverFromIndex = recoverFromIndex ?? steps.length;
|
|
405
453
|
}
|
|
@@ -412,6 +460,25 @@ var DataModel = class DataModel {
|
|
|
412
460
|
return new DataModel(state);
|
|
413
461
|
}
|
|
414
462
|
/**
|
|
463
|
+
* Reference to the block kind this data model was built for, or `undefined`
|
|
464
|
+
* for a kind-less builder. Used by `BlockModelV3.create` to cross-check that
|
|
465
|
+
* the kind handed to the builder matches the kind handed to `create`.
|
|
466
|
+
* @internal
|
|
467
|
+
*/
|
|
468
|
+
get kindRef() {
|
|
469
|
+
return this._kindRef;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The kind's runtime params check, or `undefined` if the kind declares none (or
|
|
473
|
+
* there is no kind). Read by `BlockModelV3.done()` to register the check, and
|
|
474
|
+
* carried here rather than only on the model because `init` — the one place params
|
|
475
|
+
* are consumed — lives on this side.
|
|
476
|
+
* @internal
|
|
477
|
+
*/
|
|
478
|
+
get templateParamsParser() {
|
|
479
|
+
return this._parseInitializationParams;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
415
482
|
* The latest (current) version key in the migration chain.
|
|
416
483
|
*/
|
|
417
484
|
get version() {
|
|
@@ -421,14 +488,31 @@ var DataModel = class DataModel {
|
|
|
421
488
|
* Get a fresh copy of the initial data.
|
|
422
489
|
*/
|
|
423
490
|
initialData() {
|
|
424
|
-
return this.initialDataFn();
|
|
491
|
+
return this.initialDataFn({});
|
|
425
492
|
}
|
|
426
493
|
/**
|
|
427
494
|
* Get initial data wrapped with current version.
|
|
428
495
|
* Used when creating new blocks or resetting to defaults.
|
|
429
496
|
*/
|
|
430
497
|
getDefaultData() {
|
|
431
|
-
return makeVersionedData(this.latestVersion, this.initialDataFn());
|
|
498
|
+
return makeVersionedData(this.latestVersion, this.initialDataFn({}));
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get initial data built from params, wrapped with current version.
|
|
502
|
+
*
|
|
503
|
+
* The counterpart of {@link getDefaultData} for a block created from a template
|
|
504
|
+
* entry: the factory receives the entry's params instead of nothing. A factory
|
|
505
|
+
* that ignores its argument produces the same result as `getDefaultData`, which
|
|
506
|
+
* is why the two are separate methods rather than one optional argument — the
|
|
507
|
+
* caller decides which contract it is asking for, and a block that cannot honour
|
|
508
|
+
* params must not silently look like one that can.
|
|
509
|
+
*
|
|
510
|
+
* References inside `params` are already resolved to the target project's
|
|
511
|
+
* concrete ids by the time they get here; the factory never sees a
|
|
512
|
+
* template-local one.
|
|
513
|
+
*/
|
|
514
|
+
getDataFromParams(params) {
|
|
515
|
+
return makeVersionedData(this.latestVersion, this.initialDataFn({ params }));
|
|
432
516
|
}
|
|
433
517
|
recoverFrom(data, version) {
|
|
434
518
|
let currentData = this.recoverFn(version, data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block_migrations.cjs","names":["DATA_MODEL_LEGACY_VERSION"],"sources":["../src/block_migrations.ts"],"sourcesContent":["import { DATA_MODEL_LEGACY_VERSION } from \"./block_storage\";\n\nexport type DataVersionKey = string;\nexport type DataMigrateFn<From, To> = (prev: Readonly<From>) => To;\nexport type DataCreateFn<T> = () => T;\nexport type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;\n\n/**\n * Minimal interface that .transfer() accepts. PluginInstance implements this.\n * Defined here to avoid circular dependency with plugin_model.ts.\n */\nexport interface TransferTarget<Id extends string = string, TransferData = never> {\n readonly id: Id;\n /** Version key in the plugin's data model chain where transferred data enters. */\n readonly transferVersion: string;\n /** @internal Phantom field for TransferData type extraction */\n readonly __transferBrand?: TransferData;\n}\n\n/** Internal record of a single transfer step in the migration chain. */\nexport type TransferStep = {\n pluginId: string;\n /** Capture data before this step index executes. */\n beforeStepIndex: number;\n extract: (data: unknown) => unknown;\n /** Version key in the plugin's data model chain where the transferred data enters. */\n targetVersion: string;\n};\n\n/** Map of plugin ID → versioned data extracted during migration. */\nexport type TransferRecord = Record<string, DataVersioned<unknown>>;\n\n/** Versioned data wrapper for persistence */\nexport type DataVersioned<T> = {\n version: DataVersionKey;\n data: T;\n};\n\n/** Create a DataVersioned wrapper with correct shape */\nexport function makeVersionedData<T>(version: DataVersionKey, data: T): DataVersioned<T> {\n return { version, data };\n}\n\n/** Thrown when a migration step fails. */\nexport class DataMigrationError extends Error {\n name = \"DataMigrationError\";\n constructor(message: string) {\n super(message);\n }\n}\n\n/** Thrown by recover() to signal unrecoverable data. */\nexport class DataUnrecoverableError extends Error {\n name = \"DataUnrecoverableError\";\n constructor(dataVersion: DataVersionKey) {\n super(`Unknown version '${dataVersion}'`);\n }\n}\n\nexport function isDataUnrecoverableError(error: unknown): error is DataUnrecoverableError {\n return error instanceof Error && error.name === \"DataUnrecoverableError\";\n}\n\ntype MigrationStep = {\n fromVersion: DataVersionKey;\n toVersion: DataVersionKey;\n migrate: (data: unknown) => unknown;\n};\n\n/**\n * Default recover function for unknown versions.\n * Use as fallback at the end of custom recover functions.\n *\n * @example\n * .recover((version, data) => {\n * if (version === 'legacy') {\n * return transformLegacyData(data);\n * }\n * return defaultRecover(version, data);\n * })\n */\nexport const defaultRecover: DataRecoverFn<never> = (version, _data) => {\n throw new DataUnrecoverableError(version);\n};\n\n/** Symbol for internal builder creation method */\nconst FROM_BUILDER = Symbol(\"fromBuilder\");\n\n/** Legacy V1 model state shape: { args, uiState } */\nexport type LegacyV1State<Args, UiState> = { args: Args; uiState: UiState };\n\n/** Internal state passed from builder to DataModel */\ntype BuilderState<S> = {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n transferSteps: TransferStep[];\n initialDataFn: () => S;\n recoverFn?: (version: DataVersionKey, data: unknown) => unknown;\n /** Index of the first step to run after recovery. Equals the number of steps\n * present at the time recover() was called. */\n recoverFromIndex?: number;\n};\n\ntype RecoverState = {\n recoverFn?: (version: DataVersionKey, data: unknown) => unknown;\n recoverFromIndex?: number;\n};\n\n/**\n * Abstract base for both migration chain types.\n * Holds shared state, buildStep() helper, and init().\n * migrate() cannot be shared due to a TypeScript limitation: when the base class\n * migrate() return type is abstract, subclasses cannot narrow it without losing type safety.\n * Each subclass therefore owns its migrate() with the correct concrete return type.\n *\n * @internal\n */\nabstract class MigrationChainBase<Current, Transfers extends Record<string, unknown> = {}> {\n protected readonly versionChain: DataVersionKey[];\n protected readonly migrationSteps: MigrationStep[];\n protected readonly transferSteps: TransferStep[];\n\n protected constructor(state: {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n transferSteps?: TransferStep[];\n }) {\n this.versionChain = state.versionChain;\n this.migrationSteps = state.steps;\n this.transferSteps = state.transferSteps ?? [];\n }\n\n /** Appends a migration step and returns the new versionChain and steps arrays. */\n protected buildStep<Next>(\n nextVersion: string,\n fn: DataMigrateFn<Current, Next>,\n ): { versionChain: DataVersionKey[]; steps: MigrationStep[] } {\n if (this.versionChain.includes(nextVersion)) {\n throw new Error(`Duplicate version '${nextVersion}' in migration chain`);\n }\n const fromVersion = this.versionChain[this.versionChain.length - 1];\n const step: MigrationStep = {\n fromVersion,\n toVersion: nextVersion,\n migrate: fn as (data: unknown) => unknown,\n };\n return {\n versionChain: [...this.versionChain, nextVersion],\n steps: [...this.migrationSteps, step],\n };\n }\n\n /** Validates uniqueness and records a TransferStep. */\n protected buildTransfer<Id extends string, L>(\n target: TransferTarget<Id, L>,\n extract: (data: Current) => L,\n ): { transferSteps: TransferStep[] } {\n if (this.transferSteps.some((t) => t.pluginId === target.id)) {\n throw new Error(`Duplicate transfer for plugin '${target.id}'`);\n }\n const entry: TransferStep = {\n pluginId: target.id,\n beforeStepIndex: this.migrationSteps.length,\n extract: extract as (data: unknown) => unknown,\n targetVersion: target.transferVersion,\n };\n return { transferSteps: [...this.transferSteps, entry] };\n }\n\n /** Returns recover-specific fields for DataModel construction. Overridden by WithRecover. */\n protected recoverState(): RecoverState {\n return {};\n }\n\n /**\n * Finalize the DataModel with initial data factory.\n *\n * @param initialData - Factory function returning the initial state\n * @returns Finalized DataModel instance\n */\n init(initialData: DataCreateFn<Current>): DataModel<Current, Transfers> {\n return DataModel[FROM_BUILDER]<Current, Transfers>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps: this.transferSteps,\n initialDataFn: initialData,\n ...this.recoverState(),\n });\n }\n}\n\n/**\n * Migration chain after recover() or upgradeLegacy() has been called.\n * Further migrate() and transfer() calls are allowed; recover() and upgradeLegacy() are not\n * (enforced by type — no such methods on this class).\n *\n * @typeParam Current - Data type at the current point in the chain\n * @typeParam Transfers - Accumulated transfer types keyed by plugin ID\n * @internal\n */\nclass DataModelMigrationChainWithRecover<\n Current,\n Transfers extends Record<string, unknown> = {},\n> extends MigrationChainBase<Current, Transfers> {\n private readonly recoverFn?: (version: DataVersionKey, data: unknown) => unknown;\n private readonly recoverFromIndex?: number;\n\n /** @internal */\n constructor(state: {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n transferSteps?: TransferStep[];\n recoverFn?: (version: DataVersionKey, data: unknown) => unknown;\n recoverFromIndex?: number;\n }) {\n super(state);\n this.recoverFn = state.recoverFn;\n this.recoverFromIndex = state.recoverFromIndex;\n }\n\n protected override recoverState(): RecoverState {\n return {\n recoverFn: this.recoverFn,\n recoverFromIndex: this.recoverFromIndex,\n };\n }\n\n /**\n * Add a migration step. Same semantics as on the base chain.\n * recover() and upgradeLegacy() are not available — one has already been called.\n */\n migrate<Next>(\n nextVersion: string,\n fn: DataMigrateFn<Current, Next>,\n ): DataModelMigrationChainWithRecover<Next, Transfers> {\n const { versionChain, steps } = this.buildStep(nextVersion, fn);\n return new DataModelMigrationChainWithRecover<Next, Transfers>({\n versionChain,\n steps,\n transferSteps: this.transferSteps,\n recoverFn: this.recoverFn,\n recoverFromIndex: this.recoverFromIndex,\n });\n }\n\n /**\n * Extract data at the current chain position for seeding a new plugin.\n * The extract function's return type must match the plugin's transfer data type.\n * Duplicate plugin IDs are rejected at both type and runtime level.\n */\n transfer<Id extends string, L>(\n target: TransferTarget<Id & (Id extends keyof Transfers ? never : string), L>,\n extract: (data: Current) => L,\n ): DataModelMigrationChainWithRecover<Current, Transfers & Record<Id, L>> {\n const { transferSteps } = this.buildTransfer(target, extract);\n return new DataModelMigrationChainWithRecover<Current, Transfers & Record<Id, L>>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps,\n recoverFn: this.recoverFn,\n recoverFromIndex: this.recoverFromIndex,\n });\n }\n}\n\n/**\n * Migration chain builder.\n * Each migrate() call advances the current data type. recover() can be called once\n * at any point — it removes itself from the returned chain so it cannot be called again.\n * Duplicate version keys throw at runtime.\n *\n * @typeParam Current - Data type at the current point in the migration chain\n * @typeParam Transfers - Accumulated transfer types keyed by plugin ID\n * @internal\n */\nclass DataModelMigrationChain<\n Current,\n Transfers extends Record<string, unknown> = {},\n> extends MigrationChainBase<Current, Transfers> {\n /** @internal */\n constructor({\n versionChain,\n steps = [],\n transferSteps = [],\n }: {\n versionChain: DataVersionKey[];\n steps?: MigrationStep[];\n transferSteps?: TransferStep[];\n }) {\n super({ versionChain, steps, transferSteps });\n }\n\n /**\n * Add a migration step transforming data from the current version to the next.\n *\n * @typeParam Next - Data type of the next version\n * @param nextVersion - Version key to migrate to (must be unique in the chain)\n * @param fn - Migration function\n * @returns Builder with the next version as current\n *\n * @example\n * .migrate<BlockDataV2>(\"v2\", (v1) => ({ ...v1, labels: [] }))\n */\n migrate<Next>(\n nextVersion: string,\n fn: DataMigrateFn<Current, Next>,\n ): DataModelMigrationChain<Next, Transfers> {\n const { versionChain, steps } = this.buildStep(nextVersion, fn);\n return new DataModelMigrationChain<Next, Transfers>({\n versionChain,\n steps,\n transferSteps: this.transferSteps,\n });\n }\n\n /**\n * Extract data at the current chain position for seeding a new plugin.\n * The extract function's return type must match the plugin's transfer data type.\n * Duplicate plugin IDs are rejected at both type and runtime level.\n *\n * Calling .transfer() on DataModelInitialChain returns DataModelMigrationChain,\n * which removes .upgradeLegacy() from the chain (preventing a problematic combination).\n *\n * @example\n * .from<V1>(\"v1\")\n * .transfer(tablePlugin, (v1) => ({ state: v1.tableState }))\n * .migrate<V2>(\"v2\", ({ tableState: _, ...rest }) => rest)\n */\n transfer<Id extends string, L>(\n target: TransferTarget<Id & (Id extends keyof Transfers ? never : string), L>,\n extract: (data: Current) => L,\n ): DataModelMigrationChain<Current, Transfers & Record<Id, L>> {\n const { transferSteps } = this.buildTransfer(target, extract);\n return new DataModelMigrationChain<Current, Transfers & Record<Id, L>>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps,\n });\n }\n\n /**\n * Set a recovery handler for unknown or legacy versions.\n *\n * The recover function is called when data has a version not in the migration chain.\n * It must return data of the type at this point in the chain (Current). Any migrate()\n * steps added after recover() will then run on the recovered data.\n *\n * Can only be called once — the returned chain has no recover() method.\n *\n * @param fn - Recovery function returning Current (the type at this chain position)\n * @returns Builder with migrate() and init() but without recover()\n *\n * @example\n * // Recover between migrations — recovered data goes through v3 migration\n * new DataModelBuilder<V1>(\"v1\")\n * .migrate<V2>(\"v2\", (v1) => ({ ...v1, label: \"\" }))\n * .recover((version, data) => {\n * if (version === 'legacy') return transformLegacy(data); // returns V2\n * return defaultRecover(version, data);\n * })\n * .migrate<V3>(\"v3\", (v2) => ({ ...v2, description: \"\" }))\n * .init(() => ({ count: 0, label: \"\", description: \"\" }));\n */\n recover(fn: DataRecoverFn<Current>): DataModelMigrationChainWithRecover<Current, Transfers> {\n return new DataModelMigrationChainWithRecover<Current, Transfers>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps: this.transferSteps,\n recoverFn: fn as (version: DataVersionKey, data: unknown) => unknown,\n recoverFromIndex: this.migrationSteps.length,\n });\n }\n}\n\n/**\n * Initial migration chain returned by `.from()`.\n * Extends DataModelMigrationChain with `upgradeLegacy()` — available only before\n * any `.migrate()` calls, since legacy data always arrives at the initial version.\n *\n * @typeParam Current - Data type at the initial version\n * @typeParam Transfers - Accumulated transfer types keyed by plugin ID\n * @internal\n */\nclass DataModelInitialChain<\n Current,\n Transfers extends Record<string, unknown> = {},\n> extends DataModelMigrationChain<Current, Transfers> {\n /**\n * Handle legacy V1 model state ({ args, uiState }) when upgrading a block from\n * BlockModel V1 to BlockModelV3.\n *\n * When a V1 block is upgraded, its stored state `{ args, uiState }` is normalized\n * to the internal default version. This method inserts a migration step from that\n * internal version to the version specified in `.from()`, using the provided typed\n * callback to transform the legacy shape. Non-legacy data passes through unchanged.\n *\n * Must be called right after `.from()` — not available after `.migrate()` calls.\n * Any `.migrate()` steps added after `upgradeLegacy()` will run on the transformed result.\n *\n * Can only be called once — the returned chain has no upgradeLegacy() method.\n * Mutually exclusive with recover().\n *\n * @typeParam Args - Type of the legacy block args\n * @typeParam UiState - Type of the legacy block uiState\n * @param fn - Typed transform from { args, uiState } to Current\n * @returns Builder with migrate() and init() but without recover() or upgradeLegacy()\n *\n * @example\n * type OldArgs = { inputFile: string; threshold: number };\n * type OldUiState = { selectedTab: string };\n * type BlockData = { inputFile: string; threshold: number; selectedTab: string };\n *\n * const dataModel = new DataModelBuilder()\n * .from<BlockData>(\"v1\")\n * .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({\n * inputFile: args.inputFile,\n * threshold: args.threshold,\n * selectedTab: uiState.selectedTab,\n * }))\n * .init(() => ({ inputFile: '', threshold: 0, selectedTab: 'main' }));\n */\n upgradeLegacy<Args, UiState = unknown>(\n fn: (legacy: LegacyV1State<Args, UiState>) => Current,\n ): DataModelMigrationChainWithRecover<Current, Transfers> {\n const wrappedFn = (data: unknown): unknown => {\n if (data !== null && typeof data === \"object\" && \"args\" in data) {\n return fn(data as LegacyV1State<Args, UiState>);\n }\n return data;\n };\n\n // Insert DATA_MODEL_LEGACY_VERSION as the true first version\n // with a migration step that transforms legacy data to the user's initial version.\n const initialVersion = this.versionChain[0];\n const step: MigrationStep = {\n fromVersion: DATA_MODEL_LEGACY_VERSION,\n toVersion: initialVersion,\n migrate: wrappedFn,\n };\n return new DataModelMigrationChainWithRecover<Current, Transfers>({\n versionChain: [DATA_MODEL_LEGACY_VERSION, ...this.versionChain],\n steps: [step, ...this.migrationSteps],\n // Shift transfer indices to account for the prepended legacy step\n transferSteps: this.transferSteps.map((t) => ({\n ...t,\n beforeStepIndex: t.beforeStepIndex + 1,\n })),\n });\n }\n}\n\n/**\n * Builder entry point for creating DataModel with type-safe migrations.\n *\n * @example\n * // Simple (no migrations):\n * const dataModel = new DataModelBuilder()\n * .from<BlockData>(\"v1\")\n * .init(() => ({ numbers: [] }));\n *\n * @example\n * // With migrations:\n * const dataModel = new DataModelBuilder()\n * .from<BlockDataV1>(\"v1\")\n * .migrate<BlockDataV2>(\"v2\", (v1) => ({ ...v1, labels: [] }))\n * .migrate<BlockDataV3>(\"v3\", (v2) => ({ ...v2, description: '' }))\n * .init(() => ({ numbers: [], labels: [], description: '' }));\n *\n * @example\n * // With recover() between migrations — recovered data goes through remaining migrations:\n * const dataModelChain = new DataModelBuilder()\n * .from<BlockDataV1>(\"v1\")\n * .migrate<BlockDataV2>(\"v2\", (v1) => ({ ...v1, labels: [] }));\n *\n * // recover() placed before the v3 migration: recovered data goes through v3\n * const dataModel = dataModelChain\n * .recover((version, data) => {\n * if (version === 'legacy' && isLegacyData(data)) return transformLegacy(data); // returns V2\n * return defaultRecover(version, data);\n * })\n * .migrate<BlockDataV3>(\"v3\", (v2) => ({ ...v2, description: '' }))\n * .init(() => ({ numbers: [], labels: [], description: '' }));\n *\n * @example\n * // With upgradeLegacy() — typed upgrade from BlockModel V1 state:\n * type OldArgs = { inputFile: string };\n * type OldUiState = { selectedTab: string };\n * type BlockData = { inputFile: string; selectedTab: string };\n *\n * const dataModel = new DataModelBuilder()\n * .from<BlockData>(\"v1\")\n * .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({\n * inputFile: args.inputFile,\n * selectedTab: uiState.selectedTab,\n * }))\n * .init(() => ({ inputFile: '', selectedTab: 'main' }));\n */\nexport class DataModelBuilder {\n /**\n * Start the migration chain with the given initial data type and version key.\n *\n * @typeParam T - Data type for the initial version\n * @param initialVersion - Version key string (e.g. \"v1\")\n * @returns Migration chain builder\n */\n from<T>(initialVersion: string): DataModelInitialChain<T> {\n return new DataModelInitialChain<T>({ versionChain: [initialVersion] });\n }\n}\n\n/**\n * DataModel defines the block's data structure, initial values, and migrations.\n * Used by BlockModelV3 to manage data state.\n *\n * Use `new DataModelBuilder()` to create a DataModel.\n *\n * @example\n * // With recover() between migrations:\n * // Recovered data (V2) goes through the v2→v3 migration automatically.\n * const dataModel = new DataModelBuilder()\n * .from<V1>(\"v1\")\n * .migrate<V2>(\"v2\", (v1) => ({ ...v1, label: \"\" }))\n * .recover((version, data) => {\n * if (version === \"legacy\") return transformLegacy(data); // returns V2\n * return defaultRecover(version, data);\n * })\n * .migrate<V3>(\"v3\", (v2) => ({ ...v2, description: \"\" }))\n * .init(() => ({ count: 0, label: \"\", description: \"\" }));\n */\nexport class DataModel<State, Transfers extends Record<string, unknown> = {}> {\n /** @internal Phantom field to anchor the Transfers type parameter. */\n declare readonly __transfers?: Transfers;\n\n /** Latest version key — O(1) access for the common \"already current\" check. */\n private readonly latestVersion: DataVersionKey;\n /** Maps each known version key to the index of the first step to run from it. O(1) lookup. */\n private readonly stepsByFromVersion: ReadonlyMap<DataVersionKey, number>;\n private readonly steps: MigrationStep[];\n private readonly transferSteps: TransferStep[];\n private readonly initialDataFn: () => State;\n private readonly recoverFn: (version: DataVersionKey, data: unknown) => unknown;\n private readonly recoverFromIndex: number;\n\n private constructor({\n versionChain,\n steps,\n transferSteps = [],\n initialDataFn,\n recoverFn = defaultRecover,\n recoverFromIndex,\n }: BuilderState<State>) {\n if (versionChain.length === 0) {\n throw new Error(\"DataModel requires at least one version key\");\n }\n this.latestVersion = versionChain[versionChain.length - 1];\n this.stepsByFromVersion = new Map(versionChain.map((v, i) => [v, i]));\n this.steps = steps;\n this.transferSteps = transferSteps;\n this.initialDataFn = initialDataFn;\n this.recoverFn = recoverFn;\n this.recoverFromIndex = recoverFromIndex ?? steps.length;\n }\n\n /**\n * Internal method for creating DataModel from builder.\n * Uses Symbol key to prevent external access.\n * @internal\n */\n static [FROM_BUILDER]<S, T extends Record<string, unknown> = {}>(\n state: BuilderState<S>,\n ): DataModel<S, T> {\n return new DataModel<S, T>(state);\n }\n\n /**\n * The latest (current) version key in the migration chain.\n */\n get version(): DataVersionKey {\n return this.latestVersion;\n }\n\n /**\n * Get a fresh copy of the initial data.\n */\n initialData(): State {\n return this.initialDataFn();\n }\n\n /**\n * Get initial data wrapped with current version.\n * Used when creating new blocks or resetting to defaults.\n */\n getDefaultData(): DataVersioned<State> {\n return makeVersionedData(this.latestVersion, this.initialDataFn());\n }\n\n private recoverFrom(data: unknown, version: DataVersionKey): DataVersioned<State> {\n // Step 1: call the recover function to get data at the recover point\n // Let errors (including DataUnrecoverableError) propagate to the caller.\n let currentData: unknown = this.recoverFn(version, data);\n\n // Step 2: run any migrations that were added after recover() in the chain\n for (let i = this.recoverFromIndex; i < this.steps.length; i++) {\n const step = this.steps[i];\n currentData = step.migrate(currentData);\n }\n\n return { version: this.latestVersion, data: currentData as State };\n }\n\n /**\n * Migrate versioned data from any version to the latest.\n * Collects transfer extractions at their designated chain positions.\n *\n * - If version is in chain, applies needed migrations (O(1) lookup)\n * - If version is unknown, attempts recovery; falls back to initial data\n * - If a migration step fails, throws so the caller can preserve original data\n *\n * Transfers only fire during normal step-by-step migration:\n * - Recovery path: returns empty transfers\n * - Fast-path (already at latest): returns empty transfers\n *\n * @param versioned - Data with version tag\n * @returns Migrated data at the latest version with transfer record\n * @throws If a migration step from a known version fails\n */\n migrate(versioned: DataVersioned<unknown>): DataVersioned<State> & { transfers: TransferRecord } {\n const { version: fromVersion, data } = versioned;\n\n // Fast path: already at latest version\n if (fromVersion === this.latestVersion) {\n return { version: this.latestVersion, data: data as State, transfers: {} };\n }\n\n // Unknown version: recovery path — empty transfers\n const startIndex = this.stepsByFromVersion.get(fromVersion);\n if (startIndex === undefined) {\n try {\n return { ...this.recoverFrom(data, fromVersion), transfers: {} };\n } catch {\n // Recovery failed (unknown version, recover fn threw, or post-recover\n // migration failed) — reset to initial data rather than blocking the update.\n return { ...this.getDefaultData(), transfers: {} };\n }\n }\n\n let currentData: unknown = data;\n const transfers: TransferRecord = {};\n\n // Run steps and check transfer entries before each step\n for (let i = startIndex; i < this.steps.length; i++) {\n for (const t of this.transferSteps) {\n if (t.beforeStepIndex === i) {\n transfers[t.pluginId] = {\n version: t.targetVersion,\n data: t.extract(currentData),\n };\n }\n }\n currentData = this.steps[i].migrate(currentData);\n }\n\n // Check for transfers positioned at or past the end of the steps array\n // (e.g., .transfer() was the last call before .init(), after all .migrate() calls)\n for (const t of this.transferSteps) {\n if (t.beforeStepIndex >= this.steps.length && t.beforeStepIndex >= startIndex) {\n transfers[t.pluginId] = {\n version: t.targetVersion,\n data: t.extract(currentData),\n };\n }\n }\n\n return { version: this.latestVersion, data: currentData as State, transfers };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"block_migrations.cjs","names":["DATA_MODEL_LEGACY_VERSION","#kindRef","#parseInitializationParams"],"sources":["../src/block_migrations.ts"],"sourcesContent":["import type { BlockKindReference } from \"@milaboratories/pl-model-common\";\nimport { formatKindRef } from \"@milaboratories/pl-model-common\";\nimport type { CompiledBlockKind } from \"@platforma-sdk/block-kind\";\nimport { DATA_MODEL_LEGACY_VERSION } from \"./block_storage\";\n\nexport type DataVersionKey = string;\nexport type DataMigrateFn<From, To> = (prev: Readonly<From>) => To;\n/**\n * Initial-data factory. Object-arg so future inputs (services, resolved refs)\n * can extend it without a signature break. `Params` carries the block's\n * kind-declared params type; `never` means the block reads no params yet.\n */\nexport type DataCreateFn<T, Params = never> = (args: { params?: Params }) => T;\nexport type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;\n\n/**\n * The compiled block-kind object a block declares — exactly the output of\n * `defineBlockKind` from `@platforma-sdk/block-kind`, consumed type-only.\n *\n * The kind object carries no reference field: the `{name}@{version}` reference\n * is derived from it internally (via `formatKindRef`) at the point the builder\n * and `BlockModelV3.create` need it. Aliased here so `DataModelBuilder` /\n * `create` signatures directly accept `defineBlockKind`'s output.\n */\nexport type BlockKind<Params = never> = CompiledBlockKind<Params>;\n\n/**\n * Minimal interface that .transfer() accepts. PluginInstance implements this.\n * Defined here to avoid circular dependency with plugin_model.ts.\n */\nexport interface TransferTarget<Id extends string = string, TransferData = never> {\n readonly id: Id;\n /** Version key in the plugin's data model chain where transferred data enters. */\n readonly transferVersion: string;\n /** @internal Phantom field for TransferData type extraction */\n readonly __transferBrand?: TransferData;\n}\n\n/** Internal record of a single transfer step in the migration chain. */\nexport type TransferStep = {\n pluginId: string;\n /** Capture data before this step index executes. */\n beforeStepIndex: number;\n extract: (data: unknown) => unknown;\n /** Version key in the plugin's data model chain where the transferred data enters. */\n targetVersion: string;\n};\n\n/** Map of plugin ID → versioned data extracted during migration. */\nexport type TransferRecord = Record<string, DataVersioned<unknown>>;\n\n/** Versioned data wrapper for persistence */\nexport type DataVersioned<T> = {\n version: DataVersionKey;\n data: T;\n};\n\n/** Create a DataVersioned wrapper with correct shape */\nexport function makeVersionedData<T>(version: DataVersionKey, data: T): DataVersioned<T> {\n return { version, data };\n}\n\n/** Thrown when a migration step fails. */\nexport class DataMigrationError extends Error {\n name = \"DataMigrationError\";\n constructor(message: string) {\n super(message);\n }\n}\n\n/** Thrown by recover() to signal unrecoverable data. */\nexport class DataUnrecoverableError extends Error {\n name = \"DataUnrecoverableError\";\n constructor(dataVersion: DataVersionKey) {\n super(`Unknown version '${dataVersion}'`);\n }\n}\n\nexport function isDataUnrecoverableError(error: unknown): error is DataUnrecoverableError {\n return error instanceof Error && error.name === \"DataUnrecoverableError\";\n}\n\ntype MigrationStep = {\n fromVersion: DataVersionKey;\n toVersion: DataVersionKey;\n migrate: (data: unknown) => unknown;\n};\n\n/**\n * Default recover function for unknown versions.\n * Use as fallback at the end of custom recover functions.\n *\n * @example\n * .recover((version, data) => {\n * if (version === 'legacy') {\n * return transformLegacyData(data);\n * }\n * return defaultRecover(version, data);\n * })\n */\nexport const defaultRecover: DataRecoverFn<never> = (version, _data) => {\n throw new DataUnrecoverableError(version);\n};\n\n/** Symbol for internal builder creation method */\nconst FROM_BUILDER = Symbol(\"fromBuilder\");\n\n/** Legacy V1 model state shape: { args, uiState } */\nexport type LegacyV1State<Args, UiState> = { args: Args; uiState: UiState };\n\n/**\n * The kind a chain belongs to, threaded unchanged from the builder into `DataModel`.\n *\n * Named because every constructor in this file carries exactly this pair, and they travel\n * together by construction: a reference with no parser could not check the params it names,\n * and a parser with no reference would have nothing to check them against.\n */\ntype KindWiring = {\n /** Reference to the block kind this data model belongs to, if declared. */\n kindRef?: BlockKindReference;\n /** The kind's runtime params check, threaded with {@link KindWiring.kindRef}. */\n parseInitializationParams?: (value: unknown) => unknown;\n};\n\ntype RecoverState = {\n recoverFn?: (version: DataVersionKey, data: unknown) => unknown;\n /** Index of the first step to run after recovery. Equals the number of steps\n * present at the time recover() was called. */\n recoverFromIndex?: number;\n};\n\n/** Internal state passed from builder to DataModel */\ntype BuilderState<S> = KindWiring &\n RecoverState & {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n transferSteps: TransferStep[];\n /**\n * The params type is erased to `unknown` here: the builder checks the block's\n * factory against its kind's params, but a caller that supplies params carries them as\n * data it did not type — parsed file content, today — and cannot state the type. The\n * kind's own parser is what recovers it.\n */\n initialDataFn: DataCreateFn<S, unknown>;\n };\n\n/**\n * Abstract base for both migration chain types.\n * Holds shared state, buildStep() helper, and init().\n * migrate() cannot be shared due to a TypeScript limitation: when the base class\n * migrate() return type is abstract, subclasses cannot narrow it without losing type safety.\n * Each subclass therefore owns its migrate() with the correct concrete return type.\n *\n * @internal\n */\nabstract class MigrationChainBase<\n Current,\n Transfers extends Record<string, unknown> = {},\n Params = never,\n> {\n protected readonly versionChain: DataVersionKey[];\n protected readonly migrationSteps: MigrationStep[];\n protected readonly transferSteps: TransferStep[];\n /** Kind reference seeded by the builder, threaded through the chain into init(). */\n protected readonly kindRef?: BlockKindReference;\n /** The kind's runtime params check, carried for `init()` to hand to the DataModel. */\n protected readonly parseInitializationParams?: (value: unknown) => unknown;\n\n protected constructor(\n state: KindWiring & {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n transferSteps?: TransferStep[];\n },\n ) {\n this.versionChain = state.versionChain;\n this.migrationSteps = state.steps;\n this.transferSteps = state.transferSteps ?? [];\n this.kindRef = state.kindRef;\n this.parseInitializationParams = state.parseInitializationParams;\n }\n\n /** Appends a migration step and returns the new versionChain and steps arrays. */\n protected buildStep<Next>(\n nextVersion: string,\n fn: DataMigrateFn<Current, Next>,\n ): { versionChain: DataVersionKey[]; steps: MigrationStep[] } {\n if (this.versionChain.includes(nextVersion)) {\n throw new Error(`Duplicate version '${nextVersion}' in migration chain`);\n }\n const fromVersion = this.versionChain[this.versionChain.length - 1];\n const step: MigrationStep = {\n fromVersion,\n toVersion: nextVersion,\n migrate: fn as (data: unknown) => unknown,\n };\n return {\n versionChain: [...this.versionChain, nextVersion],\n steps: [...this.migrationSteps, step],\n };\n }\n\n /** Validates uniqueness and records a TransferStep. */\n protected buildTransfer<Id extends string, L>(\n target: TransferTarget<Id, L>,\n extract: (data: Current) => L,\n ): { transferSteps: TransferStep[] } {\n if (this.transferSteps.some((t) => t.pluginId === target.id)) {\n throw new Error(`Duplicate transfer for plugin '${target.id}'`);\n }\n const entry: TransferStep = {\n pluginId: target.id,\n beforeStepIndex: this.migrationSteps.length,\n extract: extract as (data: unknown) => unknown,\n targetVersion: target.transferVersion,\n };\n return { transferSteps: [...this.transferSteps, entry] };\n }\n\n /** Returns recover-specific fields for DataModel construction. Overridden by WithRecover. */\n protected recoverState(): RecoverState {\n return {};\n }\n\n /**\n * Finalize the DataModel with initial data factory.\n *\n * @param initialData - Factory function returning the initial state\n * @returns Finalized DataModel instance\n */\n init(initialData: DataCreateFn<Current, Params>): DataModel<Current, Params, Transfers> {\n return DataModel[FROM_BUILDER]<Current, Params, Transfers>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps: this.transferSteps,\n initialDataFn: initialData as DataCreateFn<Current, unknown>,\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n ...this.recoverState(),\n });\n }\n}\n\n/**\n * Migration chain after recover() or upgradeLegacy() has been called.\n * Further migrate() and transfer() calls are allowed; recover() and upgradeLegacy() are not\n * (enforced by type — no such methods on this class).\n *\n * @typeParam Current - Data type at the current point in the chain\n * @typeParam Transfers - Accumulated transfer types keyed by plugin ID\n * @internal\n */\nclass DataModelMigrationChainWithRecover<\n Current,\n Transfers extends Record<string, unknown> = {},\n Params = never,\n> extends MigrationChainBase<Current, Transfers, Params> {\n private readonly recoverFn?: (version: DataVersionKey, data: unknown) => unknown;\n private readonly recoverFromIndex?: number;\n\n /** @internal */\n constructor(\n state: KindWiring &\n RecoverState & {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n transferSteps?: TransferStep[];\n },\n ) {\n super(state);\n this.recoverFn = state.recoverFn;\n this.recoverFromIndex = state.recoverFromIndex;\n }\n\n protected override recoverState(): RecoverState {\n return {\n recoverFn: this.recoverFn,\n recoverFromIndex: this.recoverFromIndex,\n };\n }\n\n /**\n * Add a migration step. Same semantics as on the base chain.\n * recover() and upgradeLegacy() are not available — one has already been called.\n */\n migrate<Next>(\n nextVersion: string,\n fn: DataMigrateFn<Current, Next>,\n ): DataModelMigrationChainWithRecover<Next, Transfers, Params> {\n const { versionChain, steps } = this.buildStep(nextVersion, fn);\n return new DataModelMigrationChainWithRecover<Next, Transfers, Params>({\n versionChain,\n steps,\n transferSteps: this.transferSteps,\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n recoverFn: this.recoverFn,\n recoverFromIndex: this.recoverFromIndex,\n });\n }\n\n /**\n * Extract data at the current chain position for seeding a new plugin.\n * The extract function's return type must match the plugin's transfer data type.\n * Duplicate plugin IDs are rejected at both type and runtime level.\n */\n transfer<Id extends string, L>(\n target: TransferTarget<Id & (Id extends keyof Transfers ? never : string), L>,\n extract: (data: Current) => L,\n ): DataModelMigrationChainWithRecover<Current, Transfers & Record<Id, L>, Params> {\n const { transferSteps } = this.buildTransfer(target, extract);\n return new DataModelMigrationChainWithRecover<Current, Transfers & Record<Id, L>, Params>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps,\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n recoverFn: this.recoverFn,\n recoverFromIndex: this.recoverFromIndex,\n });\n }\n}\n\n/**\n * Migration chain builder.\n * Each migrate() call advances the current data type. recover() can be called once\n * at any point — it removes itself from the returned chain so it cannot be called again.\n * Duplicate version keys throw at runtime.\n *\n * @typeParam Current - Data type at the current point in the migration chain\n * @typeParam Transfers - Accumulated transfer types keyed by plugin ID\n * @internal\n */\nclass DataModelMigrationChain<\n Current,\n Transfers extends Record<string, unknown> = {},\n Params = never,\n> extends MigrationChainBase<Current, Transfers, Params> {\n /** @internal */\n constructor({\n versionChain,\n steps = [],\n transferSteps = [],\n kindRef,\n parseInitializationParams,\n }: KindWiring & {\n versionChain: DataVersionKey[];\n steps?: MigrationStep[];\n transferSteps?: TransferStep[];\n }) {\n super({ versionChain, steps, transferSteps, kindRef, parseInitializationParams });\n }\n\n /**\n * Add a migration step transforming data from the current version to the next.\n *\n * @typeParam Next - Data type of the next version\n * @param nextVersion - Version key to migrate to (must be unique in the chain)\n * @param fn - Migration function\n * @returns Builder with the next version as current\n *\n * @example\n * .migrate<BlockDataV2>(\"v2\", (v1) => ({ ...v1, labels: [] }))\n */\n migrate<Next>(\n nextVersion: string,\n fn: DataMigrateFn<Current, Next>,\n ): DataModelMigrationChain<Next, Transfers, Params> {\n const { versionChain, steps } = this.buildStep(nextVersion, fn);\n return new DataModelMigrationChain<Next, Transfers, Params>({\n versionChain,\n steps,\n transferSteps: this.transferSteps,\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n });\n }\n\n /**\n * Extract data at the current chain position for seeding a new plugin.\n * The extract function's return type must match the plugin's transfer data type.\n * Duplicate plugin IDs are rejected at both type and runtime level.\n *\n * Calling .transfer() on DataModelInitialChain returns DataModelMigrationChain,\n * which removes .upgradeLegacy() from the chain (preventing a problematic combination).\n *\n * @example\n * .from<V1>(\"v1\")\n * .transfer(tablePlugin, (v1) => ({ state: v1.tableState }))\n * .migrate<V2>(\"v2\", ({ tableState: _, ...rest }) => rest)\n */\n transfer<Id extends string, L>(\n target: TransferTarget<Id & (Id extends keyof Transfers ? never : string), L>,\n extract: (data: Current) => L,\n ): DataModelMigrationChain<Current, Transfers & Record<Id, L>, Params> {\n const { transferSteps } = this.buildTransfer(target, extract);\n return new DataModelMigrationChain<Current, Transfers & Record<Id, L>, Params>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps,\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n });\n }\n\n /**\n * Set a recovery handler for unknown or legacy versions.\n *\n * The recover function is called when data has a version not in the migration chain.\n * It must return data of the type at this point in the chain (Current). Any migrate()\n * steps added after recover() will then run on the recovered data.\n *\n * Can only be called once — the returned chain has no recover() method.\n *\n * @param fn - Recovery function returning Current (the type at this chain position)\n * @returns Builder with migrate() and init() but without recover()\n *\n * @example\n * // Recover between migrations — recovered data goes through v3 migration\n * new DataModelBuilder<V1>(\"v1\")\n * .migrate<V2>(\"v2\", (v1) => ({ ...v1, label: \"\" }))\n * .recover((version, data) => {\n * if (version === 'legacy') return transformLegacy(data); // returns V2\n * return defaultRecover(version, data);\n * })\n * .migrate<V3>(\"v3\", (v2) => ({ ...v2, description: \"\" }))\n * .init(() => ({ count: 0, label: \"\", description: \"\" }));\n */\n recover(\n fn: DataRecoverFn<Current>,\n ): DataModelMigrationChainWithRecover<Current, Transfers, Params> {\n return new DataModelMigrationChainWithRecover<Current, Transfers, Params>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n transferSteps: this.transferSteps,\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n recoverFn: fn as (version: DataVersionKey, data: unknown) => unknown,\n recoverFromIndex: this.migrationSteps.length,\n });\n }\n}\n\n/**\n * Initial migration chain returned by `.from()`.\n * Extends DataModelMigrationChain with `upgradeLegacy()` — available only before\n * any `.migrate()` calls, since legacy data always arrives at the initial version.\n *\n * @typeParam Current - Data type at the initial version\n * @typeParam Transfers - Accumulated transfer types keyed by plugin ID\n * @internal\n */\nclass DataModelInitialChain<\n Current,\n Transfers extends Record<string, unknown> = {},\n Params = never,\n> extends DataModelMigrationChain<Current, Transfers, Params> {\n /**\n * Handle legacy V1 model state ({ args, uiState }) when upgrading a block from\n * BlockModel V1 to BlockModelV3.\n *\n * When a V1 block is upgraded, its stored state `{ args, uiState }` is normalized\n * to the internal default version. This method inserts a migration step from that\n * internal version to the version specified in `.from()`, using the provided typed\n * callback to transform the legacy shape. Non-legacy data passes through unchanged.\n *\n * Must be called right after `.from()` — not available after `.migrate()` calls.\n * Any `.migrate()` steps added after `upgradeLegacy()` will run on the transformed result.\n *\n * Can only be called once — the returned chain has no upgradeLegacy() method.\n * Mutually exclusive with recover().\n *\n * @typeParam Args - Type of the legacy block args\n * @typeParam UiState - Type of the legacy block uiState\n * @param fn - Typed transform from { args, uiState } to Current\n * @returns Builder with migrate() and init() but without recover() or upgradeLegacy()\n *\n * @example\n * type OldArgs = { inputFile: string; threshold: number };\n * type OldUiState = { selectedTab: string };\n * type BlockData = { inputFile: string; threshold: number; selectedTab: string };\n *\n * const dataModel = new DataModelBuilder()\n * .from<BlockData>(\"v1\")\n * .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({\n * inputFile: args.inputFile,\n * threshold: args.threshold,\n * selectedTab: uiState.selectedTab,\n * }))\n * .init(() => ({ inputFile: '', threshold: 0, selectedTab: 'main' }));\n */\n upgradeLegacy<Args, UiState = unknown>(\n fn: (legacy: LegacyV1State<Args, UiState>) => Current,\n ): DataModelMigrationChainWithRecover<Current, Transfers, Params> {\n const wrappedFn = (data: unknown): unknown => {\n if (data !== null && typeof data === \"object\" && \"args\" in data) {\n return fn(data as LegacyV1State<Args, UiState>);\n }\n return data;\n };\n\n // Insert DATA_MODEL_LEGACY_VERSION as the true first version\n // with a migration step that transforms legacy data to the user's initial version.\n const initialVersion = this.versionChain[0];\n const step: MigrationStep = {\n fromVersion: DATA_MODEL_LEGACY_VERSION,\n toVersion: initialVersion,\n migrate: wrappedFn,\n };\n return new DataModelMigrationChainWithRecover<Current, Transfers, Params>({\n versionChain: [DATA_MODEL_LEGACY_VERSION, ...this.versionChain],\n steps: [step, ...this.migrationSteps],\n kindRef: this.kindRef,\n parseInitializationParams: this.parseInitializationParams,\n // Shift transfer indices to account for the prepended legacy step\n transferSteps: this.transferSteps.map((t) => ({\n ...t,\n beforeStepIndex: t.beforeStepIndex + 1,\n })),\n });\n }\n}\n\n/**\n * Builder entry point for creating DataModel with type-safe migrations.\n *\n * @example\n * // Simple (no migrations):\n * const dataModel = new DataModelBuilder()\n * .from<BlockData>(\"v1\")\n * .init(() => ({ numbers: [] }));\n *\n * @example\n * // With migrations:\n * const dataModel = new DataModelBuilder()\n * .from<BlockDataV1>(\"v1\")\n * .migrate<BlockDataV2>(\"v2\", (v1) => ({ ...v1, labels: [] }))\n * .migrate<BlockDataV3>(\"v3\", (v2) => ({ ...v2, description: '' }))\n * .init(() => ({ numbers: [], labels: [], description: '' }));\n *\n * @example\n * // With recover() between migrations — recovered data goes through remaining migrations:\n * const dataModelChain = new DataModelBuilder()\n * .from<BlockDataV1>(\"v1\")\n * .migrate<BlockDataV2>(\"v2\", (v1) => ({ ...v1, labels: [] }));\n *\n * // recover() placed before the v3 migration: recovered data goes through v3\n * const dataModel = dataModelChain\n * .recover((version, data) => {\n * if (version === 'legacy' && isLegacyData(data)) return transformLegacy(data); // returns V2\n * return defaultRecover(version, data);\n * })\n * .migrate<BlockDataV3>(\"v3\", (v2) => ({ ...v2, description: '' }))\n * .init(() => ({ numbers: [], labels: [], description: '' }));\n *\n * @example\n * // With upgradeLegacy() — typed upgrade from BlockModel V1 state:\n * type OldArgs = { inputFile: string };\n * type OldUiState = { selectedTab: string };\n * type BlockData = { inputFile: string; selectedTab: string };\n *\n * const dataModel = new DataModelBuilder()\n * .from<BlockData>(\"v1\")\n * .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({\n * inputFile: args.inputFile,\n * selectedTab: uiState.selectedTab,\n * }))\n * .init(() => ({ inputFile: '', selectedTab: 'main' }));\n */\nexport class DataModelBuilder<Params = never> {\n readonly #kindRef?: BlockKindReference;\n readonly #parseInitializationParams?: (value: unknown) => Params;\n\n /**\n * @param opts.kind - The block kind this data model implements. Its reference\n * is captured and baked into the config so the manifest can advertise which\n * kind the block satisfies, and its `Params` type flows into `.init()`.\n * Optional during the transition window while existing V3 blocks are\n * migrated to kind-carrying builders; a kind-less builder simply carries no\n * reference and the reconciler can't project it yet. Object form mirrors\n * `BlockModelV3.create({ dataModel, kind })`.\n */\n constructor(opts?: { kind?: BlockKind<Params> }) {\n // Derive the on-wire `{name}@{version}` reference from the compiled kind;\n // the kind object itself has no reference field.\n this.#kindRef = opts?.kind ? formatKindRef(opts.kind) : undefined;\n // Carried alongside the reference, and for the same reason: the kind object is\n // not kept, but two things off it are needed later — how to name the kind, and\n // how to check params claimed to be of it.\n this.#parseInitializationParams = opts?.kind?.parseInitializationParams;\n }\n\n /**\n * Start the migration chain with the given initial data type and version key.\n *\n * @typeParam T - Data type for the initial version\n * @param initialVersion - Version key string (e.g. \"v1\")\n * @returns Migration chain builder\n */\n from<T>(initialVersion: string): DataModelInitialChain<T, {}, Params> {\n return new DataModelInitialChain<T, {}, Params>({\n versionChain: [initialVersion],\n kindRef: this.#kindRef,\n parseInitializationParams: this.#parseInitializationParams as\n | ((value: unknown) => unknown)\n | undefined,\n });\n }\n}\n\n/**\n * DataModel defines the block's data structure, initial values, and migrations.\n * Used by BlockModelV3 to manage data state.\n *\n * Use `new DataModelBuilder()` to create a DataModel.\n *\n * @example\n * // With recover() between migrations:\n * // Recovered data (V2) goes through the v2→v3 migration automatically.\n * const dataModel = new DataModelBuilder()\n * .from<V1>(\"v1\")\n * .migrate<V2>(\"v2\", (v1) => ({ ...v1, label: \"\" }))\n * .recover((version, data) => {\n * if (version === \"legacy\") return transformLegacy(data); // returns V2\n * return defaultRecover(version, data);\n * })\n * .migrate<V3>(\"v3\", (v2) => ({ ...v2, description: \"\" }))\n * .init(() => ({ count: 0, label: \"\", description: \"\" }));\n */\nexport class DataModel<State, Params = never, Transfers extends Record<string, unknown> = {}> {\n /** @internal Phantom field to anchor the Transfers type parameter. */\n declare readonly __transfers?: Transfers;\n /** @internal Phantom field to anchor the Params type parameter (drives the\n * compile-time kind cross-check in `BlockModelV3.create`). */\n declare readonly __params?: Params;\n\n /** Latest version key — O(1) access for the common \"already current\" check. */\n private readonly latestVersion: DataVersionKey;\n /** Maps each known version key to the index of the first step to run from it. O(1) lookup. */\n private readonly stepsByFromVersion: ReadonlyMap<DataVersionKey, number>;\n private readonly steps: MigrationStep[];\n private readonly transferSteps: TransferStep[];\n private readonly initialDataFn: DataCreateFn<State, unknown>;\n private readonly recoverFn: (version: DataVersionKey, data: unknown) => unknown;\n private readonly recoverFromIndex: number;\n /** Reference to the block kind this data model was built for, if any. */\n private readonly _kindRef?: BlockKindReference;\n /** The kind's runtime params check, if it declares one. */\n private readonly _parseInitializationParams?: (value: unknown) => unknown;\n\n private constructor({\n versionChain,\n steps,\n transferSteps = [],\n initialDataFn,\n kindRef,\n parseInitializationParams,\n recoverFn = defaultRecover,\n recoverFromIndex,\n }: BuilderState<State>) {\n if (versionChain.length === 0) {\n throw new Error(\"DataModel requires at least one version key\");\n }\n this.latestVersion = versionChain[versionChain.length - 1];\n this.stepsByFromVersion = new Map(versionChain.map((v, i) => [v, i]));\n this.steps = steps;\n this.transferSteps = transferSteps;\n this.initialDataFn = initialDataFn;\n this._kindRef = kindRef;\n this._parseInitializationParams = parseInitializationParams;\n this.recoverFn = recoverFn;\n this.recoverFromIndex = recoverFromIndex ?? steps.length;\n }\n\n /**\n * Internal method for creating DataModel from builder.\n * Uses Symbol key to prevent external access.\n * @internal\n */\n static [FROM_BUILDER]<S, P = never, T extends Record<string, unknown> = {}>(\n state: BuilderState<S>,\n ): DataModel<S, P, T> {\n return new DataModel<S, P, T>(state);\n }\n\n /**\n * Reference to the block kind this data model was built for, or `undefined`\n * for a kind-less builder. Used by `BlockModelV3.create` to cross-check that\n * the kind handed to the builder matches the kind handed to `create`.\n * @internal\n */\n get kindRef(): BlockKindReference | undefined {\n return this._kindRef;\n }\n\n /**\n * The kind's runtime params check, or `undefined` if the kind declares none (or\n * there is no kind). Read by `BlockModelV3.done()` to register the check, and\n * carried here rather than only on the model because `init` — the one place params\n * are consumed — lives on this side.\n * @internal\n */\n get templateParamsParser(): ((value: unknown) => unknown) | undefined {\n return this._parseInitializationParams;\n }\n\n /**\n * The latest (current) version key in the migration chain.\n */\n get version(): DataVersionKey {\n return this.latestVersion;\n }\n\n /**\n * Get a fresh copy of the initial data.\n */\n initialData(): State {\n return this.initialDataFn({});\n }\n\n /**\n * Get initial data wrapped with current version.\n * Used when creating new blocks or resetting to defaults.\n */\n getDefaultData(): DataVersioned<State> {\n return makeVersionedData(this.latestVersion, this.initialDataFn({}));\n }\n\n /**\n * Get initial data built from params, wrapped with current version.\n *\n * The counterpart of {@link getDefaultData} for a block created from a template\n * entry: the factory receives the entry's params instead of nothing. A factory\n * that ignores its argument produces the same result as `getDefaultData`, which\n * is why the two are separate methods rather than one optional argument — the\n * caller decides which contract it is asking for, and a block that cannot honour\n * params must not silently look like one that can.\n *\n * References inside `params` are already resolved to the target project's\n * concrete ids by the time they get here; the factory never sees a\n * template-local one.\n */\n getDataFromParams(params: unknown): DataVersioned<State> {\n return makeVersionedData(this.latestVersion, this.initialDataFn({ params }));\n }\n\n private recoverFrom(data: unknown, version: DataVersionKey): DataVersioned<State> {\n // Step 1: call the recover function to get data at the recover point\n // Let errors (including DataUnrecoverableError) propagate to the caller.\n let currentData: unknown = this.recoverFn(version, data);\n\n // Step 2: run any migrations that were added after recover() in the chain\n for (let i = this.recoverFromIndex; i < this.steps.length; i++) {\n const step = this.steps[i];\n currentData = step.migrate(currentData);\n }\n\n return { version: this.latestVersion, data: currentData as State };\n }\n\n /**\n * Migrate versioned data from any version to the latest.\n * Collects transfer extractions at their designated chain positions.\n *\n * - If version is in chain, applies needed migrations (O(1) lookup)\n * - If version is unknown, attempts recovery; falls back to initial data\n * - If a migration step fails, throws so the caller can preserve original data\n *\n * Transfers only fire during normal step-by-step migration:\n * - Recovery path: returns empty transfers\n * - Fast-path (already at latest): returns empty transfers\n *\n * @param versioned - Data with version tag\n * @returns Migrated data at the latest version with transfer record\n * @throws If a migration step from a known version fails\n */\n migrate(versioned: DataVersioned<unknown>): DataVersioned<State> & { transfers: TransferRecord } {\n const { version: fromVersion, data } = versioned;\n\n // Fast path: already at latest version\n if (fromVersion === this.latestVersion) {\n return { version: this.latestVersion, data: data as State, transfers: {} };\n }\n\n // Unknown version: recovery path — empty transfers\n const startIndex = this.stepsByFromVersion.get(fromVersion);\n if (startIndex === undefined) {\n try {\n return { ...this.recoverFrom(data, fromVersion), transfers: {} };\n } catch {\n // Recovery failed (unknown version, recover fn threw, or post-recover\n // migration failed) — reset to initial data rather than blocking the update.\n return { ...this.getDefaultData(), transfers: {} };\n }\n }\n\n let currentData: unknown = data;\n const transfers: TransferRecord = {};\n\n // Run steps and check transfer entries before each step\n for (let i = startIndex; i < this.steps.length; i++) {\n for (const t of this.transferSteps) {\n if (t.beforeStepIndex === i) {\n transfers[t.pluginId] = {\n version: t.targetVersion,\n data: t.extract(currentData),\n };\n }\n }\n currentData = this.steps[i].migrate(currentData);\n }\n\n // Check for transfers positioned at or past the end of the steps array\n // (e.g., .transfer() was the last call before .init(), after all .migrate() calls)\n for (const t of this.transferSteps) {\n if (t.beforeStepIndex >= this.steps.length && t.beforeStepIndex >= startIndex) {\n transfers[t.pluginId] = {\n version: t.targetVersion,\n data: t.extract(currentData),\n };\n }\n }\n\n return { version: this.latestVersion, data: currentData as State, transfers };\n }\n}\n"],"mappings":"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"}
|