@platforma-sdk/model 1.58.0 → 1.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/block_migrations.cjs +60 -77
- package/dist/block_migrations.cjs.map +1 -1
- package/dist/block_migrations.d.ts +35 -32
- package/dist/block_migrations.d.ts.map +1 -1
- package/dist/block_migrations.js +60 -78
- package/dist/block_migrations.js.map +1 -1
- package/dist/block_model.cjs +1 -1
- package/dist/block_model.cjs.map +1 -1
- package/dist/block_model.d.ts +1 -1
- package/dist/block_model.js +1 -1
- package/dist/block_model.js.map +1 -1
- package/dist/block_storage.cjs +6 -6
- package/dist/block_storage.cjs.map +1 -1
- package/dist/block_storage.d.ts +7 -7
- package/dist/block_storage.d.ts.map +1 -1
- package/dist/block_storage.js +6 -6
- package/dist/block_storage.js.map +1 -1
- package/dist/block_storage_callbacks.cjs +1 -0
- package/dist/block_storage_callbacks.cjs.map +1 -1
- package/dist/block_storage_callbacks.d.ts +4 -3
- package/dist/block_storage_callbacks.d.ts.map +1 -1
- package/dist/block_storage_callbacks.js +1 -0
- package/dist/block_storage_callbacks.js.map +1 -1
- package/dist/index.cjs +1 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/plugin_model.cjs +2 -2
- package/dist/plugin_model.cjs.map +1 -1
- package/dist/plugin_model.d.ts +2 -2
- package/dist/plugin_model.js +2 -2
- package/dist/plugin_model.js.map +1 -1
- package/package.json +5 -5
- package/src/block_migrations.test.ts +109 -12
- package/src/block_migrations.ts +63 -87
- package/src/block_model.ts +1 -1
- package/src/block_storage.test.ts +8 -8
- package/src/block_storage.ts +10 -10
- package/src/block_storage_callbacks.ts +4 -3
- package/src/index.ts +22 -1
- package/src/plugin_model.test.ts +2 -2
- package/src/plugin_model.ts +2 -2
package/src/block_migrations.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DATA_MODEL_LEGACY_VERSION } from "./block_storage";
|
|
2
|
+
|
|
1
3
|
export type DataVersionKey = string;
|
|
2
4
|
export type DataMigrateFn<From, To> = (prev: Readonly<From>) => To;
|
|
3
5
|
export type DataCreateFn<T> = () => T;
|
|
@@ -14,10 +16,13 @@ export function makeDataVersioned<T>(version: DataVersionKey, data: T): DataVers
|
|
|
14
16
|
return { version, data };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Thrown when a migration step fails. */
|
|
20
|
+
export class DataMigrationError extends Error {
|
|
21
|
+
name = "DataMigrationError";
|
|
22
|
+
constructor(message: string) {
|
|
23
|
+
super(message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
/** Thrown by recover() to signal unrecoverable data. */
|
|
23
28
|
export class DataUnrecoverableError extends Error {
|
|
@@ -68,14 +73,11 @@ type BuilderState<S> = {
|
|
|
68
73
|
/** Index of the first step to run after recovery. Equals the number of steps
|
|
69
74
|
* present at the time recover() was called. */
|
|
70
75
|
recoverFromIndex?: number;
|
|
71
|
-
/** Transforms legacy V1 model data ({ args, uiState }) at the initial version. */
|
|
72
|
-
upgradeLegacyFn?: (data: unknown) => unknown;
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
type RecoverState = {
|
|
76
79
|
recoverFn?: (version: DataVersionKey, data: unknown) => unknown;
|
|
77
80
|
recoverFromIndex?: number;
|
|
78
|
-
upgradeLegacyFn?: (data: unknown) => unknown;
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
/**
|
|
@@ -148,7 +150,6 @@ abstract class MigrationChainBase<Current> {
|
|
|
148
150
|
class DataModelMigrationChainWithRecover<Current> extends MigrationChainBase<Current> {
|
|
149
151
|
private readonly recoverFn?: (version: DataVersionKey, data: unknown) => unknown;
|
|
150
152
|
private readonly recoverFromIndex?: number;
|
|
151
|
-
private readonly upgradeLegacyFn?: (data: unknown) => unknown;
|
|
152
153
|
|
|
153
154
|
/** @internal */
|
|
154
155
|
constructor(state: {
|
|
@@ -156,19 +157,16 @@ class DataModelMigrationChainWithRecover<Current> extends MigrationChainBase<Cur
|
|
|
156
157
|
steps: MigrationStep[];
|
|
157
158
|
recoverFn?: (version: DataVersionKey, data: unknown) => unknown;
|
|
158
159
|
recoverFromIndex?: number;
|
|
159
|
-
upgradeLegacyFn?: (data: unknown) => unknown;
|
|
160
160
|
}) {
|
|
161
161
|
super(state);
|
|
162
162
|
this.recoverFn = state.recoverFn;
|
|
163
163
|
this.recoverFromIndex = state.recoverFromIndex;
|
|
164
|
-
this.upgradeLegacyFn = state.upgradeLegacyFn;
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
protected override recoverState(): RecoverState {
|
|
168
167
|
return {
|
|
169
168
|
recoverFn: this.recoverFn,
|
|
170
169
|
recoverFromIndex: this.recoverFromIndex,
|
|
171
|
-
upgradeLegacyFn: this.upgradeLegacyFn,
|
|
172
170
|
};
|
|
173
171
|
}
|
|
174
172
|
|
|
@@ -186,7 +184,6 @@ class DataModelMigrationChainWithRecover<Current> extends MigrationChainBase<Cur
|
|
|
186
184
|
steps,
|
|
187
185
|
recoverFn: this.recoverFn,
|
|
188
186
|
recoverFromIndex: this.recoverFromIndex,
|
|
189
|
-
upgradeLegacyFn: this.upgradeLegacyFn,
|
|
190
187
|
});
|
|
191
188
|
}
|
|
192
189
|
}
|
|
@@ -239,10 +236,9 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
239
236
|
* steps added after recover() will then run on the recovered data.
|
|
240
237
|
*
|
|
241
238
|
* Can only be called once — the returned chain has no recover() method.
|
|
242
|
-
* Mutually exclusive with upgradeLegacy().
|
|
243
239
|
*
|
|
244
240
|
* @param fn - Recovery function returning Current (the type at this chain position)
|
|
245
|
-
* @returns Builder with migrate() and init() but without recover()
|
|
241
|
+
* @returns Builder with migrate() and init() but without recover()
|
|
246
242
|
*
|
|
247
243
|
* @example
|
|
248
244
|
* // Recover between migrations — recovered data goes through v3 migration
|
|
@@ -263,19 +259,28 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
263
259
|
recoverFromIndex: this.migrationSteps.length,
|
|
264
260
|
});
|
|
265
261
|
}
|
|
262
|
+
}
|
|
266
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Initial migration chain returned by `.from()`.
|
|
266
|
+
* Extends DataModelMigrationChain with `upgradeLegacy()` — available only before
|
|
267
|
+
* any `.migrate()` calls, since legacy data always arrives at the initial version.
|
|
268
|
+
*
|
|
269
|
+
* @typeParam Current - Data type at the initial version
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
class DataModelInitialChain<Current> extends DataModelMigrationChain<Current> {
|
|
267
273
|
/**
|
|
268
274
|
* Handle legacy V1 model state ({ args, uiState }) when upgrading a block from
|
|
269
275
|
* BlockModel V1 to BlockModelV3.
|
|
270
276
|
*
|
|
271
|
-
* When a V1 block is upgraded, its stored state `{ args, uiState }`
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
277
|
+
* When a V1 block is upgraded, its stored state `{ args, uiState }` is normalized
|
|
278
|
+
* to the internal default version. This method inserts a migration step from that
|
|
279
|
+
* internal version to the version specified in `.from()`, using the provided typed
|
|
280
|
+
* callback to transform the legacy shape. Non-legacy data passes through unchanged.
|
|
275
281
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* `upgradeLegacy()` will run on the transformed result.
|
|
282
|
+
* Must be called right after `.from()` — not available after `.migrate()` calls.
|
|
283
|
+
* Any `.migrate()` steps added after `upgradeLegacy()` will run on the transformed result.
|
|
279
284
|
*
|
|
280
285
|
* Can only be called once — the returned chain has no upgradeLegacy() method.
|
|
281
286
|
* Mutually exclusive with recover().
|
|
@@ -291,7 +296,7 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
291
296
|
* type BlockData = { inputFile: string; threshold: number; selectedTab: string };
|
|
292
297
|
*
|
|
293
298
|
* const dataModel = new DataModelBuilder()
|
|
294
|
-
* .from<BlockData>(
|
|
299
|
+
* .from<BlockData>("v1")
|
|
295
300
|
* .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
|
|
296
301
|
* inputFile: args.inputFile,
|
|
297
302
|
* threshold: args.threshold,
|
|
@@ -308,10 +313,18 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
308
313
|
}
|
|
309
314
|
return data;
|
|
310
315
|
};
|
|
316
|
+
|
|
317
|
+
// Insert DATA_MODEL_LEGACY_VERSION as the true first version
|
|
318
|
+
// with a migration step that transforms legacy data to the user's initial version.
|
|
319
|
+
const initialVersion = this.versionChain[0];
|
|
320
|
+
const step: MigrationStep = {
|
|
321
|
+
fromVersion: DATA_MODEL_LEGACY_VERSION,
|
|
322
|
+
toVersion: initialVersion,
|
|
323
|
+
migrate: wrappedFn,
|
|
324
|
+
};
|
|
311
325
|
return new DataModelMigrationChainWithRecover<Current>({
|
|
312
|
-
versionChain: this.versionChain,
|
|
313
|
-
steps: this.migrationSteps,
|
|
314
|
-
upgradeLegacyFn: wrappedFn,
|
|
326
|
+
versionChain: [DATA_MODEL_LEGACY_VERSION, ...this.versionChain],
|
|
327
|
+
steps: [step, ...this.migrationSteps],
|
|
315
328
|
});
|
|
316
329
|
}
|
|
317
330
|
}
|
|
@@ -322,13 +335,13 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
322
335
|
* @example
|
|
323
336
|
* // Simple (no migrations):
|
|
324
337
|
* const dataModel = new DataModelBuilder()
|
|
325
|
-
* .from<BlockData>(
|
|
338
|
+
* .from<BlockData>("v1")
|
|
326
339
|
* .init(() => ({ numbers: [] }));
|
|
327
340
|
*
|
|
328
341
|
* @example
|
|
329
342
|
* // With migrations:
|
|
330
343
|
* const dataModel = new DataModelBuilder()
|
|
331
|
-
* .from<BlockDataV1>(
|
|
344
|
+
* .from<BlockDataV1>("v1")
|
|
332
345
|
* .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }))
|
|
333
346
|
* .migrate<BlockDataV3>("v3", (v2) => ({ ...v2, description: '' }))
|
|
334
347
|
* .init(() => ({ numbers: [], labels: [], description: '' }));
|
|
@@ -336,7 +349,7 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
336
349
|
* @example
|
|
337
350
|
* // With recover() between migrations — recovered data goes through remaining migrations:
|
|
338
351
|
* const dataModelChain = new DataModelBuilder()
|
|
339
|
-
* .from<BlockDataV1>(
|
|
352
|
+
* .from<BlockDataV1>("v1")
|
|
340
353
|
* .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }));
|
|
341
354
|
*
|
|
342
355
|
* // recover() placed before the v3 migration: recovered data goes through v3
|
|
@@ -355,7 +368,7 @@ class DataModelMigrationChain<Current> extends MigrationChainBase<Current> {
|
|
|
355
368
|
* type BlockData = { inputFile: string; selectedTab: string };
|
|
356
369
|
*
|
|
357
370
|
* const dataModel = new DataModelBuilder()
|
|
358
|
-
* .from<BlockData>(
|
|
371
|
+
* .from<BlockData>("v1")
|
|
359
372
|
* .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
|
|
360
373
|
* inputFile: args.inputFile,
|
|
361
374
|
* selectedTab: uiState.selectedTab,
|
|
@@ -367,11 +380,11 @@ export class DataModelBuilder {
|
|
|
367
380
|
* Start the migration chain with the given initial data type and version key.
|
|
368
381
|
*
|
|
369
382
|
* @typeParam T - Data type for the initial version
|
|
370
|
-
* @param initialVersion - Version key string (e.g.
|
|
383
|
+
* @param initialVersion - Version key string (e.g. "v1")
|
|
371
384
|
* @returns Migration chain builder
|
|
372
385
|
*/
|
|
373
|
-
from<T>(initialVersion: string):
|
|
374
|
-
return new
|
|
386
|
+
from<T>(initialVersion: string): DataModelInitialChain<T> {
|
|
387
|
+
return new DataModelInitialChain<T>({ versionChain: [initialVersion] });
|
|
375
388
|
}
|
|
376
389
|
}
|
|
377
390
|
|
|
@@ -385,7 +398,7 @@ export class DataModelBuilder {
|
|
|
385
398
|
* // With recover() between migrations:
|
|
386
399
|
* // Recovered data (V2) goes through the v2→v3 migration automatically.
|
|
387
400
|
* const dataModel = new DataModelBuilder()
|
|
388
|
-
* .from<V1>(
|
|
401
|
+
* .from<V1>("v1")
|
|
389
402
|
* .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
|
|
390
403
|
* .recover((version, data) => {
|
|
391
404
|
* if (version === "legacy") return transformLegacy(data); // returns V2
|
|
@@ -403,8 +416,6 @@ export class DataModel<State> {
|
|
|
403
416
|
private readonly initialDataFn: () => State;
|
|
404
417
|
private readonly recoverFn: (version: DataVersionKey, data: unknown) => unknown;
|
|
405
418
|
private readonly recoverFromIndex: number;
|
|
406
|
-
/** Transforms legacy V1 model data at the initial version before running migrations. */
|
|
407
|
-
private readonly upgradeLegacyFn?: (data: unknown) => unknown;
|
|
408
419
|
|
|
409
420
|
private constructor({
|
|
410
421
|
versionChain,
|
|
@@ -412,7 +423,6 @@ export class DataModel<State> {
|
|
|
412
423
|
initialDataFn,
|
|
413
424
|
recoverFn = defaultRecover,
|
|
414
425
|
recoverFromIndex,
|
|
415
|
-
upgradeLegacyFn,
|
|
416
426
|
}: BuilderState<State>) {
|
|
417
427
|
if (versionChain.length === 0) {
|
|
418
428
|
throw new Error("DataModel requires at least one version key");
|
|
@@ -423,7 +433,6 @@ export class DataModel<State> {
|
|
|
423
433
|
this.initialDataFn = initialDataFn;
|
|
424
434
|
this.recoverFn = recoverFn;
|
|
425
435
|
this.recoverFromIndex = recoverFromIndex ?? steps.length;
|
|
426
|
-
this.upgradeLegacyFn = upgradeLegacyFn;
|
|
427
436
|
}
|
|
428
437
|
|
|
429
438
|
/**
|
|
@@ -457,34 +466,15 @@ export class DataModel<State> {
|
|
|
457
466
|
return makeDataVersioned(this.latestVersion, this.initialDataFn());
|
|
458
467
|
}
|
|
459
468
|
|
|
460
|
-
private recoverFrom(data: unknown, version: DataVersionKey):
|
|
469
|
+
private recoverFrom(data: unknown, version: DataVersionKey): DataVersioned<State> {
|
|
461
470
|
// Step 1: call the recover function to get data at the recover point
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
currentData = this.recoverFn(version, data);
|
|
465
|
-
} catch (error) {
|
|
466
|
-
if (isDataUnrecoverableError(error)) {
|
|
467
|
-
return { ...this.getDefaultData(), warning: error.message };
|
|
468
|
-
}
|
|
469
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
470
|
-
return {
|
|
471
|
-
...this.getDefaultData(),
|
|
472
|
-
warning: `Recover failed for version '${version}': ${errorMessage}`,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
471
|
+
// Let errors (including DataUnrecoverableError) propagate to the caller.
|
|
472
|
+
let currentData: unknown = this.recoverFn(version, data);
|
|
475
473
|
|
|
476
474
|
// Step 2: run any migrations that were added after recover() in the chain
|
|
477
475
|
for (let i = this.recoverFromIndex; i < this.steps.length; i++) {
|
|
478
476
|
const step = this.steps[i];
|
|
479
|
-
|
|
480
|
-
currentData = step.migrate(currentData);
|
|
481
|
-
} catch (error) {
|
|
482
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
483
|
-
return {
|
|
484
|
-
...this.getDefaultData(),
|
|
485
|
-
warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
477
|
+
currentData = step.migrate(currentData);
|
|
488
478
|
}
|
|
489
479
|
|
|
490
480
|
return { version: this.latestVersion, data: currentData as State };
|
|
@@ -494,13 +484,14 @@ export class DataModel<State> {
|
|
|
494
484
|
* Migrate versioned data from any version to the latest.
|
|
495
485
|
*
|
|
496
486
|
* - If version is in chain, applies needed migrations (O(1) lookup)
|
|
497
|
-
* - If version is unknown,
|
|
498
|
-
* - If migration
|
|
487
|
+
* - If version is unknown, attempts recovery; falls back to initial data
|
|
488
|
+
* - If a migration step fails, throws so the caller can preserve original data
|
|
499
489
|
*
|
|
500
490
|
* @param versioned - Data with version tag
|
|
501
|
-
* @returns
|
|
491
|
+
* @returns Migrated data at the latest version
|
|
492
|
+
* @throws If a migration step from a known version fails
|
|
502
493
|
*/
|
|
503
|
-
migrate(versioned: DataVersioned<unknown>):
|
|
494
|
+
migrate(versioned: DataVersioned<unknown>): DataVersioned<State> {
|
|
504
495
|
const { version: fromVersion, data } = versioned;
|
|
505
496
|
|
|
506
497
|
if (fromVersion === this.latestVersion) {
|
|
@@ -509,35 +500,20 @@ export class DataModel<State> {
|
|
|
509
500
|
|
|
510
501
|
const startIndex = this.stepsByFromVersion.get(fromVersion);
|
|
511
502
|
if (startIndex === undefined) {
|
|
512
|
-
return this.recoverFrom(data, fromVersion);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
let currentData: unknown = data;
|
|
516
|
-
|
|
517
|
-
// Legacy V1 upgrade: detect and transform { args, uiState } at the initial version
|
|
518
|
-
if (startIndex === 0 && this.upgradeLegacyFn) {
|
|
519
503
|
try {
|
|
520
|
-
|
|
521
|
-
} catch
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
warning: `Legacy upgrade failed: ${errorMessage}`,
|
|
526
|
-
};
|
|
504
|
+
return this.recoverFrom(data, fromVersion);
|
|
505
|
+
} catch {
|
|
506
|
+
// Recovery failed (unknown version, recover fn threw, or post-recover
|
|
507
|
+
// migration failed) — reset to initial data rather than blocking the update.
|
|
508
|
+
return this.getDefaultData();
|
|
527
509
|
}
|
|
528
510
|
}
|
|
529
511
|
|
|
512
|
+
let currentData: unknown = data;
|
|
513
|
+
|
|
530
514
|
for (let i = startIndex; i < this.steps.length; i++) {
|
|
531
515
|
const step = this.steps[i];
|
|
532
|
-
|
|
533
|
-
currentData = step.migrate(currentData);
|
|
534
|
-
} catch (error) {
|
|
535
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
536
|
-
return {
|
|
537
|
-
...this.getDefaultData(),
|
|
538
|
-
warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
516
|
+
currentData = step.migrate(currentData);
|
|
541
517
|
}
|
|
542
518
|
|
|
543
519
|
return { version: this.latestVersion, data: currentData as State };
|
package/src/block_model.ts
CHANGED
|
@@ -114,7 +114,7 @@ export class BlockModelV3<
|
|
|
114
114
|
*
|
|
115
115
|
* @example
|
|
116
116
|
* const dataModel = new DataModelBuilder()
|
|
117
|
-
* .from<BlockData>(
|
|
117
|
+
* .from<BlockData>("v1")
|
|
118
118
|
* .init(() => ({ numbers: [], labels: [] }));
|
|
119
119
|
*
|
|
120
120
|
* BlockModelV3.create(dataModel)
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
BLOCK_STORAGE_KEY,
|
|
4
4
|
BLOCK_STORAGE_SCHEMA_VERSION,
|
|
5
|
-
|
|
5
|
+
DATA_MODEL_LEGACY_VERSION,
|
|
6
6
|
createBlockStorage,
|
|
7
7
|
getPluginData,
|
|
8
8
|
getStorageData,
|
|
@@ -72,7 +72,7 @@ describe("BlockStorage", () => {
|
|
|
72
72
|
it("should create storage with discriminator key and default values", () => {
|
|
73
73
|
const storage = createBlockStorage();
|
|
74
74
|
expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
75
|
-
expect(storage.__dataVersion).toBe(
|
|
75
|
+
expect(storage.__dataVersion).toBe(DATA_MODEL_LEGACY_VERSION);
|
|
76
76
|
expect(storage.__data).toEqual({});
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -80,7 +80,7 @@ describe("BlockStorage", () => {
|
|
|
80
80
|
const data = { numbers: [1, 2, 3] };
|
|
81
81
|
const storage = createBlockStorage(data);
|
|
82
82
|
expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
83
|
-
expect(storage.__dataVersion).toBe(
|
|
83
|
+
expect(storage.__dataVersion).toBe(DATA_MODEL_LEGACY_VERSION);
|
|
84
84
|
expect(storage.__data).toEqual(data);
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -136,7 +136,7 @@ describe("BlockStorage", () => {
|
|
|
136
136
|
const legacyData = { numbers: [1, 2, 3], name: "test" };
|
|
137
137
|
const normalized = normalizeBlockStorage(legacyData);
|
|
138
138
|
expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
139
|
-
expect(normalized.__dataVersion).toBe(
|
|
139
|
+
expect(normalized.__dataVersion).toBe(DATA_MODEL_LEGACY_VERSION);
|
|
140
140
|
expect(normalized.__data).toEqual(legacyData);
|
|
141
141
|
expect(normalized.__pluginRegistry).toEqual({});
|
|
142
142
|
expect(normalized.__plugins).toEqual({});
|
|
@@ -145,14 +145,14 @@ describe("BlockStorage", () => {
|
|
|
145
145
|
it("should wrap primitive legacy data", () => {
|
|
146
146
|
const normalized = normalizeBlockStorage("simple string");
|
|
147
147
|
expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
148
|
-
expect(normalized.__dataVersion).toBe(
|
|
148
|
+
expect(normalized.__dataVersion).toBe(DATA_MODEL_LEGACY_VERSION);
|
|
149
149
|
expect(normalized.__data).toBe("simple string");
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
it("should wrap null legacy data", () => {
|
|
153
153
|
const normalized = normalizeBlockStorage(null);
|
|
154
154
|
expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
155
|
-
expect(normalized.__dataVersion).toBe(
|
|
155
|
+
expect(normalized.__dataVersion).toBe(DATA_MODEL_LEGACY_VERSION);
|
|
156
156
|
expect(normalized.__data).toBeNull();
|
|
157
157
|
});
|
|
158
158
|
});
|
|
@@ -188,7 +188,7 @@ describe("BlockStorage", () => {
|
|
|
188
188
|
});
|
|
189
189
|
expect(storage.__plugins).toEqual({
|
|
190
190
|
table1: {
|
|
191
|
-
__dataVersion:
|
|
191
|
+
__dataVersion: DATA_MODEL_LEGACY_VERSION,
|
|
192
192
|
__data: { columns: ["a", "b"] },
|
|
193
193
|
},
|
|
194
194
|
});
|
|
@@ -254,7 +254,7 @@ describe("BlockStorage", () => {
|
|
|
254
254
|
value: { foo: "bar" },
|
|
255
255
|
});
|
|
256
256
|
expect(updated.__plugins?.plugin1).toEqual({
|
|
257
|
-
__dataVersion:
|
|
257
|
+
__dataVersion: DATA_MODEL_LEGACY_VERSION,
|
|
258
258
|
__data: { foo: "bar" },
|
|
259
259
|
});
|
|
260
260
|
});
|
package/src/block_storage.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Branded } from "@milaboratories/pl-model-common";
|
|
13
|
-
import type {
|
|
13
|
+
import type { DataVersioned } from "./block_migrations";
|
|
14
14
|
import type { PluginHandle, PluginFactoryLike, InferFactoryData } from "./plugin_handle";
|
|
15
15
|
|
|
16
16
|
// =============================================================================
|
|
@@ -33,7 +33,7 @@ export const BLOCK_STORAGE_SCHEMA_VERSION = "v1";
|
|
|
33
33
|
* Default data version for new blocks without migrations.
|
|
34
34
|
* Unique identifier ensures blocks are created via DataModel API.
|
|
35
35
|
*/
|
|
36
|
-
export const
|
|
36
|
+
export const DATA_MODEL_LEGACY_VERSION = "__pl_v1_d4e8f2a1__";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Type for valid schema versions
|
|
@@ -100,12 +100,12 @@ export function isBlockStorage(value: unknown): value is BlockStorage {
|
|
|
100
100
|
* Creates a BlockStorage with the given initial data
|
|
101
101
|
*
|
|
102
102
|
* @param initialData - The initial data value (defaults to empty object)
|
|
103
|
-
* @param version - The initial data version key (defaults to
|
|
103
|
+
* @param version - The initial data version key (defaults to DATA_MODEL_LEGACY_VERSION)
|
|
104
104
|
* @returns A new BlockStorage instance with discriminator key
|
|
105
105
|
*/
|
|
106
106
|
export function createBlockStorage<TState = unknown>(
|
|
107
107
|
initialData: TState = {} as TState,
|
|
108
|
-
version: string =
|
|
108
|
+
version: string = DATA_MODEL_LEGACY_VERSION,
|
|
109
109
|
): BlockStorage<TState> {
|
|
110
110
|
return {
|
|
111
111
|
[BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
|
|
@@ -132,7 +132,7 @@ export function normalizeBlockStorage<TState = unknown>(raw: unknown): BlockStor
|
|
|
132
132
|
// Fix for early released version where __dataVersion was a number
|
|
133
133
|
__dataVersion:
|
|
134
134
|
typeof storage.__dataVersion === "number"
|
|
135
|
-
?
|
|
135
|
+
? DATA_MODEL_LEGACY_VERSION
|
|
136
136
|
: storage.__dataVersion,
|
|
137
137
|
// Ensure plugin fields have defaults
|
|
138
138
|
__pluginRegistry: storage.__pluginRegistry ?? {},
|
|
@@ -197,7 +197,7 @@ export function updateStorageData<TValue = unknown>(
|
|
|
197
197
|
const { pluginId, value } = payload;
|
|
198
198
|
const currentPlugins = storage.__plugins ?? {};
|
|
199
199
|
const existingEntry = currentPlugins[pluginId];
|
|
200
|
-
const version = existingEntry?.__dataVersion ??
|
|
200
|
+
const version = existingEntry?.__dataVersion ?? DATA_MODEL_LEGACY_VERSION;
|
|
201
201
|
return {
|
|
202
202
|
...storage,
|
|
203
203
|
__plugins: {
|
|
@@ -258,13 +258,13 @@ export type MigrationResult<TState> = MigrationSuccess<TState> | MigrationFailur
|
|
|
258
258
|
* Conversion to internal VersionedData format is handled by migrateBlockStorage().
|
|
259
259
|
*/
|
|
260
260
|
export interface MigrateBlockStorageConfig {
|
|
261
|
-
/** Migrate block data from any version to latest */
|
|
262
|
-
migrateBlockData: (versioned: DataVersioned<unknown>) =>
|
|
263
|
-
/** Migrate each plugin's data. Return undefined to remove the plugin. */
|
|
261
|
+
/** Migrate block data from any version to latest. Throws on failure. */
|
|
262
|
+
migrateBlockData: (versioned: DataVersioned<unknown>) => DataVersioned<unknown>;
|
|
263
|
+
/** Migrate each plugin's data. Return undefined to remove the plugin. Throws on failure. */
|
|
264
264
|
migratePluginData: (
|
|
265
265
|
handle: PluginHandle,
|
|
266
266
|
versioned: DataVersioned<unknown>,
|
|
267
|
-
) =>
|
|
267
|
+
) => DataVersioned<unknown> | undefined;
|
|
268
268
|
/** The new plugin registry after migration (pluginId -> pluginName) */
|
|
269
269
|
newPluginRegistry: PluginRegistry;
|
|
270
270
|
/** Factory to create initial data for new plugins */
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import type { PluginHandle } from "./plugin_handle";
|
|
28
28
|
|
|
29
29
|
import { stringifyJson, type StringifiedJson } from "@milaboratories/pl-model-common";
|
|
30
|
-
import type {
|
|
30
|
+
import type { DataVersioned } from "./block_migrations";
|
|
31
31
|
|
|
32
32
|
// =============================================================================
|
|
33
33
|
// Hook interfaces for dependency injection
|
|
@@ -35,12 +35,12 @@ import type { DataMigrationResult, DataVersioned } from "./block_migrations";
|
|
|
35
35
|
|
|
36
36
|
/** Dependencies for storage migration */
|
|
37
37
|
export interface MigrationHooks {
|
|
38
|
-
migrateBlockData: (versioned: DataVersioned<unknown>) =>
|
|
38
|
+
migrateBlockData: (versioned: DataVersioned<unknown>) => DataVersioned<unknown>;
|
|
39
39
|
getPluginRegistry: () => PluginRegistry;
|
|
40
40
|
migratePluginData: (
|
|
41
41
|
handle: PluginHandle,
|
|
42
42
|
versioned: DataVersioned<unknown>,
|
|
43
|
-
) =>
|
|
43
|
+
) => DataVersioned<unknown> | undefined;
|
|
44
44
|
createPluginData: (handle: PluginHandle) => DataVersioned<unknown>;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -229,6 +229,7 @@ export function migrateStorage(
|
|
|
229
229
|
*
|
|
230
230
|
* @param hooks - Dependencies for creating initial block and plugin data
|
|
231
231
|
* @returns Initial storage as branded JSON string
|
|
232
|
+
* @throws If initialDataFn or createPluginData throws
|
|
232
233
|
*/
|
|
233
234
|
export function createInitialStorage(hooks: InitialStorageHooks): StringifiedJson<BlockStorage> {
|
|
234
235
|
const blockDefault = hooks.getDefaultBlockData();
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
export * from "./block_state_patch";
|
|
2
2
|
export * from "./block_state_util";
|
|
3
3
|
export * from "./plugin_handle";
|
|
4
|
-
export
|
|
4
|
+
export {
|
|
5
|
+
type BlockStorageSchemaVersion,
|
|
6
|
+
type PluginName,
|
|
7
|
+
type PluginRegistry,
|
|
8
|
+
type VersionedData,
|
|
9
|
+
type BlockStorage,
|
|
10
|
+
isBlockStorage,
|
|
11
|
+
createBlockStorage,
|
|
12
|
+
normalizeBlockStorage,
|
|
13
|
+
getStorageData,
|
|
14
|
+
deriveDataFromStorage,
|
|
15
|
+
type MutateStoragePayload,
|
|
16
|
+
updateStorageData,
|
|
17
|
+
type StorageDebugView,
|
|
18
|
+
type MigrationSuccess,
|
|
19
|
+
type MigrationFailure,
|
|
20
|
+
type MigrationResult,
|
|
21
|
+
type MigrateBlockStorageConfig,
|
|
22
|
+
migrateBlockStorage,
|
|
23
|
+
getPluginData,
|
|
24
|
+
} from "./block_storage";
|
|
5
25
|
export * from "./block_storage_facade";
|
|
6
26
|
export * from "./block_model_legacy";
|
|
7
27
|
export { BlockModelV3 } from "./block_model";
|
|
@@ -9,6 +29,7 @@ export type { PluginInstance, ParamsInput } from "./block_model";
|
|
|
9
29
|
export {
|
|
10
30
|
DataModel,
|
|
11
31
|
DataModelBuilder,
|
|
32
|
+
DataMigrationError,
|
|
12
33
|
DataUnrecoverableError,
|
|
13
34
|
isDataUnrecoverableError,
|
|
14
35
|
defaultRecover,
|
package/src/plugin_model.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { PluginModel } from "./plugin_model";
|
|
3
3
|
import type { PluginRenderCtx } from "./plugin_model";
|
|
4
4
|
import { DataModelBuilder } from "./block_migrations";
|
|
5
|
-
import {
|
|
5
|
+
import { type PluginName } from "./block_storage";
|
|
6
6
|
import type { ResultPool } from "./render";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
@@ -11,7 +11,7 @@ import type { ResultPool } from "./render";
|
|
|
11
11
|
|
|
12
12
|
type Data = { count: number; label: string };
|
|
13
13
|
|
|
14
|
-
const dataModelChain = new DataModelBuilder().from<Data>(
|
|
14
|
+
const dataModelChain = new DataModelBuilder().from<Data>("v1");
|
|
15
15
|
|
|
16
16
|
// Mock ResultPool for testing
|
|
17
17
|
const mockResultPool = {} as ResultPool;
|
package/src/plugin_model.ts
CHANGED
|
@@ -83,7 +83,7 @@ export class PluginModel<
|
|
|
83
83
|
* @returns PluginModelBuilder for chaining output definitions
|
|
84
84
|
*
|
|
85
85
|
* @example
|
|
86
|
-
* const dataModelChain = new DataModelBuilder().from<MyData>(
|
|
86
|
+
* const dataModelChain = new DataModelBuilder().from<MyData>("v1");
|
|
87
87
|
*
|
|
88
88
|
* const myPlugin = PluginModel.define({
|
|
89
89
|
* name: 'myPlugin' as PluginName,
|
|
@@ -169,7 +169,7 @@ class PluginModelFactory<
|
|
|
169
169
|
* @typeParam Outputs - Accumulated output types
|
|
170
170
|
*
|
|
171
171
|
* @example
|
|
172
|
-
* const dataModelChain = new DataModelBuilder().from<TableData>(
|
|
172
|
+
* const dataModelChain = new DataModelBuilder().from<TableData>("v1");
|
|
173
173
|
*
|
|
174
174
|
* const dataTable = PluginModel.define<TableData, TableParams, TableConfig>({
|
|
175
175
|
* name: 'dataTable' as PluginName,
|