@platforma-sdk/model 1.54.13 → 1.56.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/bconfig/normalization.cjs +8 -1
- package/dist/bconfig/normalization.cjs.map +1 -1
- package/dist/bconfig/normalization.d.ts.map +1 -1
- package/dist/bconfig/normalization.js +8 -1
- package/dist/bconfig/normalization.js.map +1 -1
- package/dist/block_api_v3.d.ts +2 -2
- package/dist/block_api_v3.d.ts.map +1 -1
- package/dist/block_migrations.cjs +246 -214
- package/dist/block_migrations.cjs.map +1 -1
- package/dist/block_migrations.d.ts +180 -158
- package/dist/block_migrations.d.ts.map +1 -1
- package/dist/block_migrations.js +247 -214
- package/dist/block_migrations.js.map +1 -1
- package/dist/block_model.cjs +85 -35
- package/dist/block_model.cjs.map +1 -1
- package/dist/block_model.d.ts +66 -38
- package/dist/block_model.d.ts.map +1 -1
- package/dist/block_model.js +86 -36
- package/dist/block_model.js.map +1 -1
- package/dist/{builder.cjs → block_model_legacy.cjs} +2 -2
- package/dist/block_model_legacy.cjs.map +1 -0
- package/dist/{builder.d.ts → block_model_legacy.d.ts} +1 -1
- package/dist/block_model_legacy.d.ts.map +1 -0
- package/dist/{builder.js → block_model_legacy.js} +2 -2
- package/dist/block_model_legacy.js.map +1 -0
- package/dist/block_state_patch.d.ts +11 -1
- package/dist/block_state_patch.d.ts.map +1 -1
- package/dist/block_storage.cjs +126 -109
- package/dist/block_storage.cjs.map +1 -1
- package/dist/block_storage.d.ts +109 -112
- package/dist/block_storage.d.ts.map +1 -1
- package/dist/block_storage.js +126 -101
- package/dist/block_storage.js.map +1 -1
- package/dist/block_storage_callbacks.cjs +227 -0
- package/dist/block_storage_callbacks.cjs.map +1 -0
- package/dist/block_storage_callbacks.d.ts +113 -0
- package/dist/block_storage_callbacks.d.ts.map +1 -0
- package/dist/block_storage_callbacks.js +218 -0
- package/dist/block_storage_callbacks.js.map +1 -0
- package/dist/block_storage_facade.cjs +104 -0
- package/dist/block_storage_facade.cjs.map +1 -0
- package/dist/block_storage_facade.d.ts +168 -0
- package/dist/block_storage_facade.d.ts.map +1 -0
- package/dist/block_storage_facade.js +99 -0
- package/dist/block_storage_facade.js.map +1 -0
- package/dist/index.cjs +13 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/platforma.d.ts +11 -4
- package/dist/platforma.d.ts.map +1 -1
- package/dist/plugin_model.cjs +171 -0
- package/dist/plugin_model.cjs.map +1 -0
- package/dist/plugin_model.d.ts +162 -0
- package/dist/plugin_model.d.ts.map +1 -0
- package/dist/plugin_model.js +169 -0
- package/dist/plugin_model.js.map +1 -0
- package/dist/render/api.cjs +20 -21
- package/dist/render/api.cjs.map +1 -1
- package/dist/render/api.d.ts +8 -8
- package/dist/render/api.d.ts.map +1 -1
- package/dist/render/api.js +20 -21
- package/dist/render/api.js.map +1 -1
- package/dist/render/internal.cjs.map +1 -1
- package/dist/render/internal.d.ts +2 -1
- package/dist/render/internal.d.ts.map +1 -1
- package/dist/render/internal.js.map +1 -1
- package/dist/version.cjs +4 -0
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +4 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/bconfig/normalization.ts +8 -1
- package/src/block_api_v3.ts +2 -2
- package/src/block_migrations.test.ts +141 -171
- package/src/block_migrations.ts +300 -285
- package/src/block_model.ts +205 -95
- package/src/{builder.ts → block_model_legacy.ts} +1 -1
- package/src/block_state_patch.ts +13 -1
- package/src/block_storage.test.ts +283 -95
- package/src/block_storage.ts +199 -188
- package/src/block_storage_callbacks.ts +326 -0
- package/src/block_storage_facade.ts +199 -0
- package/src/index.ts +7 -3
- package/src/platforma.ts +26 -7
- package/src/plugin_model.test.ts +168 -0
- package/src/plugin_model.ts +242 -0
- package/src/render/api.ts +26 -24
- package/src/render/internal.ts +3 -1
- package/src/typing.test.ts +1 -1
- package/src/version.ts +8 -0
- package/dist/block_storage_vm.cjs +0 -262
- package/dist/block_storage_vm.cjs.map +0 -1
- package/dist/block_storage_vm.d.ts +0 -59
- package/dist/block_storage_vm.d.ts.map +0 -1
- package/dist/block_storage_vm.js +0 -258
- package/dist/block_storage_vm.js.map +0 -1
- package/dist/branding.d.ts +0 -7
- package/dist/branding.d.ts.map +0 -1
- package/dist/builder.cjs.map +0 -1
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js.map +0 -1
- package/dist/sdk_info.cjs +0 -10
- package/dist/sdk_info.cjs.map +0 -1
- package/dist/sdk_info.d.ts +0 -5
- package/dist/sdk_info.d.ts.map +0 -1
- package/dist/sdk_info.js +0 -8
- package/dist/sdk_info.js.map +0 -1
- package/dist/unionize.d.ts +0 -12
- package/dist/unionize.d.ts.map +0 -1
- package/src/block_storage_vm.ts +0 -346
- package/src/branding.ts +0 -4
- package/src/sdk_info.ts +0 -9
- package/src/unionize.ts +0 -12
|
@@ -1,41 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var internal = require('./internal.cjs');
|
|
4
|
-
var block_storage = require('./block_storage.cjs');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Helper to define version keys with literal type inference and runtime validation.
|
|
8
|
-
* - Validates that all version values are unique
|
|
9
|
-
* - Validates that no version value is empty
|
|
10
|
-
* - Eliminates need for `as const` assertion
|
|
11
|
-
*
|
|
12
|
-
* @throws Error if duplicate or empty version values are found
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* const Version = defineDataVersions({
|
|
16
|
-
* Initial: 'v1',
|
|
17
|
-
* AddedLabels: 'v2',
|
|
18
|
-
* });
|
|
19
|
-
*
|
|
20
|
-
* type VersionedData = {
|
|
21
|
-
* [Version.Initial]: DataV1;
|
|
22
|
-
* [Version.AddedLabels]: DataV2;
|
|
23
|
-
* };
|
|
24
|
-
*/
|
|
25
|
-
function defineDataVersions(versions) {
|
|
26
|
-
const values = Object.values(versions);
|
|
27
|
-
const keys = Object.keys(versions);
|
|
28
|
-
const emptyKeys = keys.filter((key) => versions[key] === "");
|
|
29
|
-
if (emptyKeys.length > 0) {
|
|
30
|
-
throw new Error(`Version values must be non-empty strings (empty: ${emptyKeys.join(", ")})`);
|
|
31
|
-
}
|
|
32
|
-
const unique = new Set(values);
|
|
33
|
-
if (unique.size !== values.length) {
|
|
34
|
-
const duplicates = values.filter((v, i) => values.indexOf(v) !== i);
|
|
35
|
-
throw new Error(`Duplicate version values: ${[...new Set(duplicates)].join(", ")}`);
|
|
36
|
-
}
|
|
37
|
-
return versions;
|
|
38
|
-
}
|
|
39
3
|
/** Create a DataVersioned wrapper with correct shape */
|
|
40
4
|
function makeDataVersioned(version, data) {
|
|
41
5
|
return { version, data };
|
|
@@ -68,186 +32,260 @@ const defaultRecover = (version, _data) => {
|
|
|
68
32
|
/** Symbol for internal builder creation method */
|
|
69
33
|
const FROM_BUILDER = Symbol("fromBuilder");
|
|
70
34
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
35
|
+
* Abstract base for both migration chain types.
|
|
36
|
+
* Holds shared state, buildStep() helper, and init().
|
|
37
|
+
* migrate() cannot be shared due to a TypeScript limitation: when the base class
|
|
38
|
+
* migrate() return type is abstract, subclasses cannot narrow it without losing type safety.
|
|
39
|
+
* Each subclass therefore owns its migrate() with the correct concrete return type.
|
|
73
40
|
*
|
|
74
|
-
* @typeParam VersionedData - Map of version keys to their data types
|
|
75
|
-
* @typeParam CurrentVersion - The current (final) version in the chain
|
|
76
41
|
* @internal
|
|
77
42
|
*/
|
|
78
|
-
class
|
|
43
|
+
class MigrationChainBase {
|
|
79
44
|
versionChain;
|
|
80
45
|
migrationSteps;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
46
|
+
constructor(state) {
|
|
47
|
+
this.versionChain = state.versionChain;
|
|
48
|
+
this.migrationSteps = state.steps;
|
|
49
|
+
}
|
|
50
|
+
/** Appends a migration step and returns the new versionChain and steps arrays. */
|
|
51
|
+
buildStep(nextVersion, fn) {
|
|
52
|
+
if (this.versionChain.includes(nextVersion)) {
|
|
53
|
+
throw new Error(`Duplicate version '${nextVersion}' in migration chain`);
|
|
54
|
+
}
|
|
55
|
+
const fromVersion = this.versionChain[this.versionChain.length - 1];
|
|
56
|
+
const step = {
|
|
57
|
+
fromVersion,
|
|
58
|
+
toVersion: nextVersion,
|
|
59
|
+
migrate: fn,
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
versionChain: [...this.versionChain, nextVersion],
|
|
63
|
+
steps: [...this.migrationSteps, step],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Returns recover-specific fields for DataModel construction. Overridden by WithRecover. */
|
|
67
|
+
recoverState() {
|
|
68
|
+
return {};
|
|
87
69
|
}
|
|
88
70
|
/**
|
|
89
71
|
* Finalize the DataModel with initial data factory.
|
|
90
72
|
*
|
|
91
|
-
*
|
|
92
|
-
* migration/recovery fails and data must be reset.
|
|
93
|
-
*
|
|
94
|
-
* @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)
|
|
73
|
+
* @param initialData - Factory function returning the initial state
|
|
95
74
|
* @returns Finalized DataModel instance
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* .init(() => ({ numbers: [], labels: [], description: '' }))
|
|
99
75
|
*/
|
|
100
|
-
init(initialData
|
|
101
|
-
// Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
|
|
102
|
-
..._noExtraKeys) {
|
|
76
|
+
init(initialData) {
|
|
103
77
|
return DataModel[FROM_BUILDER]({
|
|
104
78
|
versionChain: this.versionChain,
|
|
105
79
|
steps: this.migrationSteps,
|
|
106
80
|
initialDataFn: initialData,
|
|
107
|
-
|
|
81
|
+
...this.recoverState(),
|
|
108
82
|
});
|
|
109
83
|
}
|
|
110
84
|
}
|
|
111
85
|
/**
|
|
112
|
-
*
|
|
86
|
+
* Migration chain after recover() or upgradeLegacy() has been called.
|
|
87
|
+
* Further migrate() calls are allowed; recover() and upgradeLegacy() are not
|
|
88
|
+
* (enforced by type — no such methods on this class).
|
|
113
89
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
90
|
+
* @typeParam Current - Data type at the current point in the chain
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
class DataModelMigrationChainWithRecover extends MigrationChainBase {
|
|
94
|
+
recoverFn;
|
|
95
|
+
recoverFromIndex;
|
|
96
|
+
upgradeLegacyFn;
|
|
97
|
+
/** @internal */
|
|
98
|
+
constructor(state) {
|
|
99
|
+
super(state);
|
|
100
|
+
this.recoverFn = state.recoverFn;
|
|
101
|
+
this.recoverFromIndex = state.recoverFromIndex;
|
|
102
|
+
this.upgradeLegacyFn = state.upgradeLegacyFn;
|
|
103
|
+
}
|
|
104
|
+
recoverState() {
|
|
105
|
+
return {
|
|
106
|
+
recoverFn: this.recoverFn,
|
|
107
|
+
recoverFromIndex: this.recoverFromIndex,
|
|
108
|
+
upgradeLegacyFn: this.upgradeLegacyFn,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Add a migration step. Same semantics as on the base chain.
|
|
113
|
+
* recover() and upgradeLegacy() are not available — one has already been called.
|
|
114
|
+
*/
|
|
115
|
+
migrate(nextVersion, fn) {
|
|
116
|
+
const { versionChain, steps } = this.buildStep(nextVersion, fn);
|
|
117
|
+
return new DataModelMigrationChainWithRecover({
|
|
118
|
+
versionChain,
|
|
119
|
+
steps,
|
|
120
|
+
recoverFn: this.recoverFn,
|
|
121
|
+
recoverFromIndex: this.recoverFromIndex,
|
|
122
|
+
upgradeLegacyFn: this.upgradeLegacyFn,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Migration chain builder.
|
|
128
|
+
* Each migrate() call advances the current data type. recover() can be called once
|
|
129
|
+
* at any point — it removes itself from the returned chain so it cannot be called again.
|
|
130
|
+
* Duplicate version keys throw at runtime.
|
|
119
131
|
*
|
|
120
|
-
* @typeParam
|
|
121
|
-
* @typeParam CurrentVersion - The current version in the migration chain
|
|
122
|
-
* @typeParam RemainingVersions - Versions not yet covered by migrations
|
|
132
|
+
* @typeParam Current - Data type at the current point in the migration chain
|
|
123
133
|
* @internal
|
|
124
134
|
*/
|
|
125
|
-
class DataModelMigrationChain {
|
|
126
|
-
versionChain;
|
|
127
|
-
migrationSteps;
|
|
135
|
+
class DataModelMigrationChain extends MigrationChainBase {
|
|
128
136
|
/** @internal */
|
|
129
137
|
constructor({ versionChain, steps = [], }) {
|
|
130
|
-
|
|
131
|
-
this.migrationSteps = steps;
|
|
138
|
+
super({ versionChain, steps });
|
|
132
139
|
}
|
|
133
140
|
/**
|
|
134
|
-
* Add a migration step
|
|
135
|
-
*
|
|
136
|
-
* Migration functions:
|
|
137
|
-
* - Receive data typed as the current version's data type (readonly)
|
|
138
|
-
* - Must return data matching the target version's data type
|
|
139
|
-
* - Should be pure functions (no side effects)
|
|
140
|
-
* - May throw errors (will result in data reset with warning)
|
|
141
|
+
* Add a migration step transforming data from the current version to the next.
|
|
141
142
|
*
|
|
142
|
-
* @typeParam
|
|
143
|
-
* @param nextVersion -
|
|
144
|
-
* @param fn - Migration function
|
|
145
|
-
* @returns Builder with
|
|
143
|
+
* @typeParam Next - Data type of the next version
|
|
144
|
+
* @param nextVersion - Version key to migrate to (must be unique in the chain)
|
|
145
|
+
* @param fn - Migration function
|
|
146
|
+
* @returns Builder with the next version as current
|
|
146
147
|
*
|
|
147
148
|
* @example
|
|
148
|
-
* .migrate(
|
|
149
|
+
* .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }))
|
|
149
150
|
*/
|
|
150
151
|
migrate(nextVersion, fn) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const fromVersion = this.versionChain[this.versionChain.length - 1];
|
|
155
|
-
const step = {
|
|
156
|
-
fromVersion,
|
|
157
|
-
toVersion: nextVersion,
|
|
158
|
-
migrate: fn,
|
|
159
|
-
};
|
|
160
|
-
return new DataModelMigrationChain({
|
|
161
|
-
versionChain: [...this.versionChain, nextVersion],
|
|
162
|
-
steps: [...this.migrationSteps, step],
|
|
163
|
-
});
|
|
152
|
+
const { versionChain, steps } = this.buildStep(nextVersion, fn);
|
|
153
|
+
return new DataModelMigrationChain({ versionChain, steps });
|
|
164
154
|
}
|
|
165
155
|
/**
|
|
166
156
|
* Set a recovery handler for unknown or legacy versions.
|
|
167
157
|
*
|
|
168
158
|
* The recover function is called when data has a version not in the migration chain.
|
|
169
|
-
* It
|
|
170
|
-
*
|
|
171
|
-
* - Call `defaultRecover(version, data)` to signal unrecoverable data
|
|
159
|
+
* It must return data of the type at this point in the chain (Current). Any migrate()
|
|
160
|
+
* steps added after recover() will then run on the recovered data.
|
|
172
161
|
*
|
|
173
|
-
* Can only be called once
|
|
162
|
+
* Can only be called once — the returned chain has no recover() method.
|
|
163
|
+
* Mutually exclusive with upgradeLegacy().
|
|
174
164
|
*
|
|
175
|
-
* @param fn - Recovery function
|
|
176
|
-
* @returns Builder with
|
|
165
|
+
* @param fn - Recovery function returning Current (the type at this chain position)
|
|
166
|
+
* @returns Builder with migrate() and init() but without recover() or upgradeLegacy()
|
|
177
167
|
*
|
|
178
168
|
* @example
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
169
|
+
* // Recover between migrations — recovered data goes through v3 migration
|
|
170
|
+
* new DataModelBuilder<V1>("v1")
|
|
171
|
+
* .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
|
|
172
|
+
* .recover((version, data) => {
|
|
173
|
+
* if (version === 'legacy') return transformLegacy(data); // returns V2
|
|
174
|
+
* return defaultRecover(version, data);
|
|
175
|
+
* })
|
|
176
|
+
* .migrate<V3>("v3", (v2) => ({ ...v2, description: "" }))
|
|
177
|
+
* .init(() => ({ count: 0, label: "", description: "" }));
|
|
185
178
|
*/
|
|
186
179
|
recover(fn) {
|
|
187
|
-
return new
|
|
188
|
-
versionChain:
|
|
189
|
-
steps:
|
|
180
|
+
return new DataModelMigrationChainWithRecover({
|
|
181
|
+
versionChain: this.versionChain,
|
|
182
|
+
steps: this.migrationSteps,
|
|
190
183
|
recoverFn: fn,
|
|
184
|
+
recoverFromIndex: this.migrationSteps.length,
|
|
191
185
|
});
|
|
192
186
|
}
|
|
193
187
|
/**
|
|
194
|
-
*
|
|
188
|
+
* Handle legacy V1 model state ({ args, uiState }) when upgrading a block from
|
|
189
|
+
* BlockModel V1 to BlockModelV3.
|
|
195
190
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
191
|
+
* When a V1 block is upgraded, its stored state `{ args, uiState }` arrives at the
|
|
192
|
+
* initial version (DATA_MODEL_DEFAULT_VERSION) in the migration chain. This method
|
|
193
|
+
* detects the legacy shape and transforms it to the current chain type using the
|
|
194
|
+
* provided typed callback. Non-legacy data passes through unchanged.
|
|
198
195
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
196
|
+
* Should be called right after `.from()` (before any `.migrate()` calls), since legacy
|
|
197
|
+
* data always arrives at the initial version. Any `.migrate()` steps added after
|
|
198
|
+
* `upgradeLegacy()` will run on the transformed result.
|
|
201
199
|
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
200
|
+
* Can only be called once — the returned chain has no upgradeLegacy() method.
|
|
201
|
+
* Mutually exclusive with recover().
|
|
202
|
+
*
|
|
203
|
+
* @typeParam Args - Type of the legacy block args
|
|
204
|
+
* @typeParam UiState - Type of the legacy block uiState
|
|
205
|
+
* @param fn - Typed transform from { args, uiState } to Current
|
|
206
|
+
* @returns Builder with migrate() and init() but without recover() or upgradeLegacy()
|
|
204
207
|
*
|
|
205
208
|
* @example
|
|
206
|
-
*
|
|
209
|
+
* type OldArgs = { inputFile: string; threshold: number };
|
|
210
|
+
* type OldUiState = { selectedTab: string };
|
|
211
|
+
* type BlockData = { inputFile: string; threshold: number; selectedTab: string };
|
|
212
|
+
*
|
|
213
|
+
* const dataModel = new DataModelBuilder()
|
|
214
|
+
* .from<BlockData>(DATA_MODEL_DEFAULT_VERSION)
|
|
215
|
+
* .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
|
|
216
|
+
* inputFile: args.inputFile,
|
|
217
|
+
* threshold: args.threshold,
|
|
218
|
+
* selectedTab: uiState.selectedTab,
|
|
219
|
+
* }))
|
|
220
|
+
* .init(() => ({ inputFile: '', threshold: 0, selectedTab: 'main' }));
|
|
207
221
|
*/
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
upgradeLegacy(fn) {
|
|
223
|
+
const wrappedFn = (data) => {
|
|
224
|
+
if (data !== null && typeof data === "object" && "args" in data) {
|
|
225
|
+
return fn(data);
|
|
226
|
+
}
|
|
227
|
+
return data;
|
|
228
|
+
};
|
|
229
|
+
return new DataModelMigrationChainWithRecover({
|
|
212
230
|
versionChain: this.versionChain,
|
|
213
231
|
steps: this.migrationSteps,
|
|
214
|
-
|
|
232
|
+
upgradeLegacyFn: wrappedFn,
|
|
215
233
|
});
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
/**
|
|
219
237
|
* Builder entry point for creating DataModel with type-safe migrations.
|
|
220
238
|
*
|
|
221
|
-
* @
|
|
239
|
+
* @example
|
|
240
|
+
* // Simple (no migrations):
|
|
241
|
+
* const dataModel = new DataModelBuilder()
|
|
242
|
+
* .from<BlockData>(DATA_MODEL_DEFAULT_VERSION)
|
|
243
|
+
* .init(() => ({ numbers: [] }));
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* // With migrations:
|
|
247
|
+
* const dataModel = new DataModelBuilder()
|
|
248
|
+
* .from<BlockDataV1>(DATA_MODEL_DEFAULT_VERSION)
|
|
249
|
+
* .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }))
|
|
250
|
+
* .migrate<BlockDataV3>("v3", (v2) => ({ ...v2, description: '' }))
|
|
251
|
+
* .init(() => ({ numbers: [], labels: [], description: '' }));
|
|
222
252
|
*
|
|
223
253
|
* @example
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* });
|
|
254
|
+
* // With recover() between migrations — recovered data goes through remaining migrations:
|
|
255
|
+
* const dataModelChain = new DataModelBuilder()
|
|
256
|
+
* .from<BlockDataV1>(DATA_MODEL_DEFAULT_VERSION)
|
|
257
|
+
* .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }));
|
|
228
258
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
259
|
+
* // recover() placed before the v3 migration: recovered data goes through v3
|
|
260
|
+
* const dataModel = dataModelChain
|
|
261
|
+
* .recover((version, data) => {
|
|
262
|
+
* if (version === 'legacy' && isLegacyData(data)) return transformLegacy(data); // returns V2
|
|
263
|
+
* return defaultRecover(version, data);
|
|
264
|
+
* })
|
|
265
|
+
* .migrate<BlockDataV3>("v3", (v2) => ({ ...v2, description: '' }))
|
|
266
|
+
* .init(() => ({ numbers: [], labels: [], description: '' }));
|
|
233
267
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
268
|
+
* @example
|
|
269
|
+
* // With upgradeLegacy() — typed upgrade from BlockModel V1 state:
|
|
270
|
+
* type OldArgs = { inputFile: string };
|
|
271
|
+
* type OldUiState = { selectedTab: string };
|
|
272
|
+
* type BlockData = { inputFile: string; selectedTab: string };
|
|
273
|
+
*
|
|
274
|
+
* const dataModel = new DataModelBuilder()
|
|
275
|
+
* .from<BlockData>(DATA_MODEL_DEFAULT_VERSION)
|
|
276
|
+
* .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
|
|
277
|
+
* inputFile: args.inputFile,
|
|
278
|
+
* selectedTab: uiState.selectedTab,
|
|
279
|
+
* }))
|
|
280
|
+
* .init(() => ({ inputFile: '', selectedTab: 'main' }));
|
|
238
281
|
*/
|
|
239
282
|
class DataModelBuilder {
|
|
240
283
|
/**
|
|
241
|
-
* Start
|
|
284
|
+
* Start the migration chain with the given initial data type and version key.
|
|
242
285
|
*
|
|
243
|
-
* @typeParam
|
|
244
|
-
* @param initialVersion -
|
|
245
|
-
* @returns Migration chain builder
|
|
246
|
-
*
|
|
247
|
-
* @example
|
|
248
|
-
* new DataModelBuilder<VersionedData>()
|
|
249
|
-
* .from(Version.V1)
|
|
250
|
-
* .migrate(Version.V2, (data) => ({ ...data, newField: '' }))
|
|
286
|
+
* @typeParam T - Data type for the initial version
|
|
287
|
+
* @param initialVersion - Version key string (e.g. DATA_MODEL_DEFAULT_VERSION or "v1")
|
|
288
|
+
* @returns Migration chain builder
|
|
251
289
|
*/
|
|
252
290
|
from(initialVersion) {
|
|
253
291
|
return new DataModelMigrationChain({ versionChain: [initialVersion] });
|
|
@@ -257,56 +295,43 @@ class DataModelBuilder {
|
|
|
257
295
|
* DataModel defines the block's data structure, initial values, and migrations.
|
|
258
296
|
* Used by BlockModelV3 to manage data state.
|
|
259
297
|
*
|
|
260
|
-
* Use `new DataModelBuilder
|
|
298
|
+
* Use `new DataModelBuilder()` to create a DataModel.
|
|
261
299
|
*
|
|
262
|
-
* **Simple (no migrations):**
|
|
263
300
|
* @example
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* .
|
|
269
|
-
* .init(() => ({ numbers: [], labels: [] }));
|
|
270
|
-
*
|
|
271
|
-
* **With migrations:**
|
|
272
|
-
* @example
|
|
273
|
-
* const Version = defineDataVersions({
|
|
274
|
-
* V1: DATA_MODEL_DEFAULT_VERSION,
|
|
275
|
-
* V2: 'v2',
|
|
276
|
-
* V3: 'v3',
|
|
277
|
-
* });
|
|
278
|
-
*
|
|
279
|
-
* type VersionedData = {
|
|
280
|
-
* [Version.V1]: { numbers: number[] };
|
|
281
|
-
* [Version.V2]: { numbers: number[]; labels: string[] };
|
|
282
|
-
* [Version.V3]: { numbers: number[]; labels: string[]; description: string };
|
|
283
|
-
* };
|
|
284
|
-
*
|
|
285
|
-
* const dataModel = new DataModelBuilder<VersionedData>()
|
|
286
|
-
* .from(Version.V1)
|
|
287
|
-
* .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
|
|
288
|
-
* .migrate(Version.V3, (data) => ({ ...data, description: '' }))
|
|
301
|
+
* // With recover() between migrations:
|
|
302
|
+
* // Recovered data (V2) goes through the v2→v3 migration automatically.
|
|
303
|
+
* const dataModel = new DataModelBuilder()
|
|
304
|
+
* .from<V1>(DATA_MODEL_DEFAULT_VERSION)
|
|
305
|
+
* .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
|
|
289
306
|
* .recover((version, data) => {
|
|
290
|
-
* if (version ===
|
|
291
|
-
* return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };
|
|
292
|
-
* }
|
|
307
|
+
* if (version === "legacy") return transformLegacy(data); // returns V2
|
|
293
308
|
* return defaultRecover(version, data);
|
|
294
309
|
* })
|
|
295
|
-
* .
|
|
310
|
+
* .migrate<V3>("v3", (v2) => ({ ...v2, description: "" }))
|
|
311
|
+
* .init(() => ({ count: 0, label: "", description: "" }));
|
|
296
312
|
*/
|
|
297
313
|
class DataModel {
|
|
298
|
-
|
|
314
|
+
/** Latest version key — O(1) access for the common "already current" check. */
|
|
315
|
+
latestVersion;
|
|
316
|
+
/** Maps each known version key to the index of the first step to run from it. O(1) lookup. */
|
|
317
|
+
stepsByFromVersion;
|
|
299
318
|
steps;
|
|
300
319
|
initialDataFn;
|
|
301
320
|
recoverFn;
|
|
302
|
-
|
|
321
|
+
recoverFromIndex;
|
|
322
|
+
/** Transforms legacy V1 model data at the initial version before running migrations. */
|
|
323
|
+
upgradeLegacyFn;
|
|
324
|
+
constructor({ versionChain, steps, initialDataFn, recoverFn = defaultRecover, recoverFromIndex, upgradeLegacyFn, }) {
|
|
303
325
|
if (versionChain.length === 0) {
|
|
304
326
|
throw new Error("DataModel requires at least one version key");
|
|
305
327
|
}
|
|
306
|
-
this.
|
|
328
|
+
this.latestVersion = versionChain[versionChain.length - 1];
|
|
329
|
+
this.stepsByFromVersion = new Map(versionChain.map((v, i) => [v, i]));
|
|
307
330
|
this.steps = steps;
|
|
308
331
|
this.initialDataFn = initialDataFn;
|
|
309
332
|
this.recoverFn = recoverFn;
|
|
333
|
+
this.recoverFromIndex = recoverFromIndex ?? steps.length;
|
|
334
|
+
this.upgradeLegacyFn = upgradeLegacyFn;
|
|
310
335
|
}
|
|
311
336
|
/**
|
|
312
337
|
* Internal method for creating DataModel from builder.
|
|
@@ -320,13 +345,7 @@ class DataModel {
|
|
|
320
345
|
* The latest (current) version key in the migration chain.
|
|
321
346
|
*/
|
|
322
347
|
get version() {
|
|
323
|
-
return this.
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Number of migration steps defined.
|
|
327
|
-
*/
|
|
328
|
-
get migrationCount() {
|
|
329
|
-
return this.steps.length;
|
|
348
|
+
return this.latestVersion;
|
|
330
349
|
}
|
|
331
350
|
/**
|
|
332
351
|
* Get a fresh copy of the initial data.
|
|
@@ -339,11 +358,13 @@ class DataModel {
|
|
|
339
358
|
* Used when creating new blocks or resetting to defaults.
|
|
340
359
|
*/
|
|
341
360
|
getDefaultData() {
|
|
342
|
-
return makeDataVersioned(this.
|
|
361
|
+
return makeDataVersioned(this.latestVersion, this.initialDataFn());
|
|
343
362
|
}
|
|
344
363
|
recoverFrom(data, version) {
|
|
364
|
+
// Step 1: call the recover function to get data at the recover point
|
|
365
|
+
let currentData;
|
|
345
366
|
try {
|
|
346
|
-
|
|
367
|
+
currentData = this.recoverFn(version, data);
|
|
347
368
|
}
|
|
348
369
|
catch (error) {
|
|
349
370
|
if (isDataUnrecoverableError(error)) {
|
|
@@ -355,13 +376,27 @@ class DataModel {
|
|
|
355
376
|
warning: `Recover failed for version '${version}': ${errorMessage}`,
|
|
356
377
|
};
|
|
357
378
|
}
|
|
379
|
+
// Step 2: run any migrations that were added after recover() in the chain
|
|
380
|
+
for (let i = this.recoverFromIndex; i < this.steps.length; i++) {
|
|
381
|
+
const step = this.steps[i];
|
|
382
|
+
try {
|
|
383
|
+
currentData = step.migrate(currentData);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
387
|
+
return {
|
|
388
|
+
...this.getDefaultData(),
|
|
389
|
+
warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return { version: this.latestVersion, data: currentData };
|
|
358
394
|
}
|
|
359
395
|
/**
|
|
360
396
|
* Migrate versioned data from any version to the latest.
|
|
361
397
|
*
|
|
362
|
-
* - If
|
|
363
|
-
* - If version is
|
|
364
|
-
* - If version is unknown, calls recover function
|
|
398
|
+
* - If version is in chain, applies needed migrations (O(1) lookup)
|
|
399
|
+
* - If version is unknown, calls recover function then runs remaining migrations
|
|
365
400
|
* - If migration/recovery fails, returns default data with warning
|
|
366
401
|
*
|
|
367
402
|
* @param versioned - Data with version tag
|
|
@@ -369,14 +404,27 @@ class DataModel {
|
|
|
369
404
|
*/
|
|
370
405
|
migrate(versioned) {
|
|
371
406
|
const { version: fromVersion, data } = versioned;
|
|
372
|
-
if (fromVersion === this.
|
|
373
|
-
return { version: this.
|
|
407
|
+
if (fromVersion === this.latestVersion) {
|
|
408
|
+
return { version: this.latestVersion, data: data };
|
|
374
409
|
}
|
|
375
|
-
const startIndex = this.
|
|
376
|
-
if (startIndex
|
|
410
|
+
const startIndex = this.stepsByFromVersion.get(fromVersion);
|
|
411
|
+
if (startIndex === undefined) {
|
|
377
412
|
return this.recoverFrom(data, fromVersion);
|
|
378
413
|
}
|
|
379
414
|
let currentData = data;
|
|
415
|
+
// Legacy V1 upgrade: detect and transform { args, uiState } at the initial version
|
|
416
|
+
if (startIndex === 0 && this.upgradeLegacyFn) {
|
|
417
|
+
try {
|
|
418
|
+
currentData = this.upgradeLegacyFn(currentData);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
422
|
+
return {
|
|
423
|
+
...this.getDefaultData(),
|
|
424
|
+
warning: `Legacy upgrade failed: ${errorMessage}`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
380
428
|
for (let i = startIndex; i < this.steps.length; i++) {
|
|
381
429
|
const step = this.steps[i];
|
|
382
430
|
try {
|
|
@@ -390,22 +438,7 @@ class DataModel {
|
|
|
390
438
|
};
|
|
391
439
|
}
|
|
392
440
|
}
|
|
393
|
-
return { version: this.
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Register callbacks for use in the VM.
|
|
397
|
-
* Called by BlockModelV3.create() to set up internal callbacks.
|
|
398
|
-
*
|
|
399
|
-
* @internal
|
|
400
|
-
*/
|
|
401
|
-
registerCallbacks() {
|
|
402
|
-
internal.tryRegisterCallback("__pl_data_initial", () => this.initialDataFn());
|
|
403
|
-
internal.tryRegisterCallback("__pl_data_upgrade", (versioned) => this.migrate(versioned));
|
|
404
|
-
internal.tryRegisterCallback("__pl_storage_initial", () => {
|
|
405
|
-
const { version, data } = this.getDefaultData();
|
|
406
|
-
const storage = block_storage.createBlockStorage(data, version);
|
|
407
|
-
return JSON.stringify(storage);
|
|
408
|
-
});
|
|
441
|
+
return { version: this.latestVersion, data: currentData };
|
|
409
442
|
}
|
|
410
443
|
}
|
|
411
444
|
|
|
@@ -413,7 +446,6 @@ exports.DataModel = DataModel;
|
|
|
413
446
|
exports.DataModelBuilder = DataModelBuilder;
|
|
414
447
|
exports.DataUnrecoverableError = DataUnrecoverableError;
|
|
415
448
|
exports.defaultRecover = defaultRecover;
|
|
416
|
-
exports.defineDataVersions = defineDataVersions;
|
|
417
449
|
exports.isDataUnrecoverableError = isDataUnrecoverableError;
|
|
418
450
|
exports.makeDataVersioned = makeDataVersioned;
|
|
419
451
|
//# sourceMappingURL=block_migrations.cjs.map
|