@platforma-sdk/model 1.53.1 → 1.53.2
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 +243 -44
- package/dist/block_migrations.cjs.map +1 -1
- package/dist/block_migrations.d.ts +218 -34
- package/dist/block_migrations.d.ts.map +1 -1
- package/dist/block_migrations.js +243 -45
- package/dist/block_migrations.js.map +1 -1
- package/dist/block_storage.cjs +7 -1
- package/dist/block_storage.cjs.map +1 -1
- package/dist/block_storage.d.ts.map +1 -1
- package/dist/block_storage.js +7 -1
- package/dist/block_storage.js.map +1 -1
- package/dist/block_storage_vm.cjs +4 -4
- package/dist/block_storage_vm.cjs.map +1 -1
- package/dist/block_storage_vm.d.ts +1 -1
- package/dist/block_storage_vm.js +4 -4
- package/dist/block_storage_vm.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/package.json +3 -3
- package/src/block_migrations.test.ts +79 -43
- package/src/block_migrations.ts +298 -84
- package/src/block_storage.ts +7 -1
- package/src/block_storage_vm.ts +4 -4
- package/src/index.ts +1 -0
package/src/block_migrations.ts
CHANGED
|
@@ -10,9 +10,10 @@ export type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;
|
|
|
10
10
|
/**
|
|
11
11
|
* Helper to define version keys with literal type inference and runtime validation.
|
|
12
12
|
* - Validates that all version values are unique
|
|
13
|
+
* - Validates that no version value is empty
|
|
13
14
|
* - Eliminates need for `as const` assertion
|
|
14
15
|
*
|
|
15
|
-
* @throws Error if duplicate version values are found
|
|
16
|
+
* @throws Error if duplicate or empty version values are found
|
|
16
17
|
*
|
|
17
18
|
* @example
|
|
18
19
|
* const Version = defineDataVersions({
|
|
@@ -26,8 +27,9 @@ export type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;
|
|
|
26
27
|
* };
|
|
27
28
|
*/
|
|
28
29
|
export function defineDataVersions<const T extends Record<string, string>>(versions: T): T {
|
|
29
|
-
const values = Object.values(versions);
|
|
30
|
-
const
|
|
30
|
+
const values = Object.values(versions) as (string & keyof T)[];
|
|
31
|
+
const keys = Object.keys(versions) as (keyof T)[];
|
|
32
|
+
const emptyKeys = keys.filter((key) => versions[key] === '');
|
|
31
33
|
if (emptyKeys.length > 0) {
|
|
32
34
|
throw new Error(`Version values must be non-empty strings (empty: ${emptyKeys.join(', ')})`);
|
|
33
35
|
}
|
|
@@ -73,76 +75,259 @@ type MigrationStep = {
|
|
|
73
75
|
migrate: (data: unknown) => unknown;
|
|
74
76
|
};
|
|
75
77
|
|
|
76
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Default recover function for unknown versions.
|
|
80
|
+
* Use as fallback at the end of custom recover functions.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* .recover((version, data) => {
|
|
84
|
+
* if (version === 'legacy') {
|
|
85
|
+
* return transformLegacyData(data);
|
|
86
|
+
* }
|
|
87
|
+
* return defaultRecover(version, data);
|
|
88
|
+
* })
|
|
89
|
+
*/
|
|
77
90
|
export const defaultRecover: DataRecoverFn<never> = (version, _data) => {
|
|
78
91
|
throw new DataUnrecoverableError(version);
|
|
79
92
|
};
|
|
80
93
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
94
|
+
/** Symbol for internal builder creation method */
|
|
95
|
+
const FROM_BUILDER = Symbol('fromBuilder');
|
|
96
|
+
|
|
97
|
+
/** Internal state passed from builder to DataModel */
|
|
98
|
+
type BuilderState<S> = {
|
|
99
|
+
versionChain: DataVersionKey[];
|
|
100
|
+
steps: MigrationStep[];
|
|
101
|
+
initialDataFn: () => S;
|
|
102
|
+
recoverFn?: DataRecoverFn<S>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Final builder state after recover() is called.
|
|
107
|
+
* Only allows calling create() to finalize the DataModel.
|
|
108
|
+
*
|
|
109
|
+
* @typeParam VersionedData - Map of version keys to their data types
|
|
110
|
+
* @typeParam CurrentVersion - The current (final) version in the chain
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
class DataModelBuilderWithRecover<
|
|
83
114
|
VersionedData extends DataVersionMap,
|
|
84
115
|
CurrentVersion extends keyof VersionedData & string,
|
|
85
116
|
> {
|
|
86
117
|
private readonly versionChain: DataVersionKey[];
|
|
87
118
|
private readonly migrationSteps: MigrationStep[];
|
|
88
|
-
private readonly recoverFn
|
|
119
|
+
private readonly recoverFn: DataRecoverFn<VersionedData[CurrentVersion]>;
|
|
89
120
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
/** @internal */
|
|
122
|
+
constructor({
|
|
123
|
+
versionChain,
|
|
124
|
+
steps,
|
|
125
|
+
recoverFn,
|
|
126
|
+
}: {
|
|
127
|
+
versionChain: DataVersionKey[];
|
|
128
|
+
steps: MigrationStep[];
|
|
129
|
+
recoverFn: DataRecoverFn<VersionedData[CurrentVersion]>;
|
|
130
|
+
}) {
|
|
95
131
|
this.versionChain = versionChain;
|
|
96
132
|
this.migrationSteps = steps;
|
|
97
133
|
this.recoverFn = recoverFn;
|
|
98
134
|
}
|
|
99
135
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Finalize the DataModel with initial data factory.
|
|
138
|
+
*
|
|
139
|
+
* The initial data factory is called when creating new blocks or when
|
|
140
|
+
* migration/recovery fails and data must be reset.
|
|
141
|
+
*
|
|
142
|
+
* @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)
|
|
143
|
+
* @returns Finalized DataModel instance
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* .init(() => ({ numbers: [], labels: [], description: '' }))
|
|
147
|
+
*/
|
|
148
|
+
init<S extends VersionedData[CurrentVersion]>(
|
|
149
|
+
initialData: DataCreateFn<S>,
|
|
150
|
+
// Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
|
|
151
|
+
..._noExtraKeys: Exclude<keyof S, keyof VersionedData[CurrentVersion]> extends never ? [] : [never]
|
|
152
|
+
): DataModel<VersionedData[CurrentVersion]> {
|
|
153
|
+
return DataModel[FROM_BUILDER]<VersionedData[CurrentVersion]>({
|
|
154
|
+
versionChain: this.versionChain,
|
|
155
|
+
steps: this.migrationSteps,
|
|
156
|
+
initialDataFn: initialData as DataCreateFn<VersionedData[CurrentVersion]>,
|
|
157
|
+
recoverFn: this.recoverFn,
|
|
158
|
+
});
|
|
106
159
|
}
|
|
160
|
+
}
|
|
107
161
|
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Internal builder for constructing DataModel with type-safe migration chains.
|
|
164
|
+
*
|
|
165
|
+
* Tracks the current version through the generic type system, ensuring:
|
|
166
|
+
* - Migration functions receive correctly typed input
|
|
167
|
+
* - Migration functions must return the correct output type
|
|
168
|
+
* - Version keys must exist in the VersionedData map
|
|
169
|
+
* - All versions must be covered before calling init()
|
|
170
|
+
*
|
|
171
|
+
* @typeParam VersionedData - Map of version keys to their data types
|
|
172
|
+
* @typeParam CurrentVersion - The current version in the migration chain
|
|
173
|
+
* @typeParam RemainingVersions - Versions not yet covered by migrations
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
class DataModelMigrationChain<
|
|
177
|
+
VersionedData extends DataVersionMap,
|
|
178
|
+
CurrentVersion extends keyof VersionedData & string,
|
|
179
|
+
RemainingVersions extends keyof VersionedData & string = Exclude<keyof VersionedData & string, CurrentVersion>,
|
|
180
|
+
> {
|
|
181
|
+
private readonly versionChain: DataVersionKey[];
|
|
182
|
+
private readonly migrationSteps: MigrationStep[];
|
|
183
|
+
|
|
184
|
+
/** @internal */
|
|
185
|
+
constructor({
|
|
186
|
+
versionChain,
|
|
187
|
+
steps = [],
|
|
188
|
+
}: {
|
|
189
|
+
versionChain: DataVersionKey[];
|
|
190
|
+
steps?: MigrationStep[];
|
|
191
|
+
}) {
|
|
192
|
+
this.versionChain = versionChain;
|
|
193
|
+
this.migrationSteps = steps;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Add a migration step to transform data from current version to next version.
|
|
198
|
+
*
|
|
199
|
+
* Migration functions:
|
|
200
|
+
* - Receive data typed as the current version's data type (readonly)
|
|
201
|
+
* - Must return data matching the target version's data type
|
|
202
|
+
* - Should be pure functions (no side effects)
|
|
203
|
+
* - May throw errors (will result in data reset with warning)
|
|
204
|
+
*
|
|
205
|
+
* @typeParam NextVersion - The target version key (must be in RemainingVersions)
|
|
206
|
+
* @param nextVersion - The version key to migrate to
|
|
207
|
+
* @param fn - Migration function transforming current data to next version
|
|
208
|
+
* @returns Builder with updated current version
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
|
|
212
|
+
*/
|
|
213
|
+
migrate<NextVersion extends RemainingVersions>(
|
|
110
214
|
nextVersion: NextVersion,
|
|
111
215
|
fn: DataMigrateFn<VersionedData[CurrentVersion], VersionedData[NextVersion]>,
|
|
112
|
-
):
|
|
216
|
+
): DataModelMigrationChain<VersionedData, NextVersion, Exclude<RemainingVersions, NextVersion>> {
|
|
113
217
|
if (this.versionChain.includes(nextVersion)) {
|
|
114
218
|
throw new Error(`Duplicate version '${nextVersion}' in migration chain`);
|
|
115
219
|
}
|
|
116
220
|
const fromVersion = this.versionChain[this.versionChain.length - 1];
|
|
117
221
|
const step: MigrationStep = { fromVersion, toVersion: nextVersion, migrate: fn as (data: unknown) => unknown };
|
|
118
|
-
return new
|
|
119
|
-
[...this.versionChain, nextVersion],
|
|
120
|
-
[...this.migrationSteps, step],
|
|
121
|
-
);
|
|
222
|
+
return new DataModelMigrationChain<VersionedData, NextVersion, Exclude<RemainingVersions, NextVersion>>({
|
|
223
|
+
versionChain: [...this.versionChain, nextVersion],
|
|
224
|
+
steps: [...this.migrationSteps, step],
|
|
225
|
+
});
|
|
122
226
|
}
|
|
123
227
|
|
|
124
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Set a recovery handler for unknown or legacy versions.
|
|
230
|
+
*
|
|
231
|
+
* The recover function is called when data has a version not in the migration chain.
|
|
232
|
+
* It should either:
|
|
233
|
+
* - Transform the data to the current version's format and return it
|
|
234
|
+
* - Call `defaultRecover(version, data)` to signal unrecoverable data
|
|
235
|
+
*
|
|
236
|
+
* Can only be called once. After calling, only `init()` is available.
|
|
237
|
+
*
|
|
238
|
+
* @param fn - Recovery function that transforms unknown data or throws
|
|
239
|
+
* @returns Builder with only init() method available
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* .recover((version, data) => {
|
|
243
|
+
* if (version === 'legacy' && isLegacyFormat(data)) {
|
|
244
|
+
* return transformLegacy(data);
|
|
245
|
+
* }
|
|
246
|
+
* return defaultRecover(version, data);
|
|
247
|
+
* })
|
|
248
|
+
*/
|
|
125
249
|
recover(
|
|
126
250
|
fn: DataRecoverFn<VersionedData[CurrentVersion]>,
|
|
127
|
-
):
|
|
128
|
-
return new
|
|
129
|
-
[...this.versionChain],
|
|
130
|
-
[...this.migrationSteps],
|
|
131
|
-
fn,
|
|
132
|
-
);
|
|
251
|
+
): DataModelBuilderWithRecover<VersionedData, CurrentVersion> {
|
|
252
|
+
return new DataModelBuilderWithRecover<VersionedData, CurrentVersion>({
|
|
253
|
+
versionChain: [...this.versionChain],
|
|
254
|
+
steps: [...this.migrationSteps],
|
|
255
|
+
recoverFn: fn,
|
|
256
|
+
});
|
|
133
257
|
}
|
|
134
258
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Finalize the DataModel with initial data factory.
|
|
261
|
+
*
|
|
262
|
+
* Can only be called when all versions in VersionedData have been covered
|
|
263
|
+
* by the migration chain (RemainingVersions is empty).
|
|
264
|
+
*
|
|
265
|
+
* The initial data factory is called when creating new blocks or when
|
|
266
|
+
* migration/recovery fails and data must be reset.
|
|
267
|
+
*
|
|
268
|
+
* @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)
|
|
269
|
+
* @returns Finalized DataModel instance
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* .init(() => ({ numbers: [], labels: [], description: '' }))
|
|
273
|
+
*/
|
|
274
|
+
init<S extends VersionedData[CurrentVersion]>(
|
|
275
|
+
// Compile-time check: RemainingVersions must be empty (all versions covered)
|
|
276
|
+
this: DataModelMigrationChain<VersionedData, CurrentVersion, never>,
|
|
137
277
|
initialData: DataCreateFn<S>,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
278
|
+
// Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
|
|
279
|
+
..._noExtraKeys: Exclude<keyof S, keyof VersionedData[CurrentVersion]> extends never ? [] : [never]
|
|
280
|
+
): DataModel<VersionedData[CurrentVersion]> {
|
|
281
|
+
return DataModel[FROM_BUILDER]<VersionedData[CurrentVersion]>({
|
|
282
|
+
versionChain: this.versionChain,
|
|
283
|
+
steps: this.migrationSteps,
|
|
284
|
+
initialDataFn: initialData as DataCreateFn<VersionedData[CurrentVersion]>,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Builder entry point for creating DataModel with type-safe migrations.
|
|
291
|
+
*
|
|
292
|
+
* @typeParam VersionedData - Map of version keys to their data types
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* const Version = defineDataVersions({
|
|
296
|
+
* V1: 'v1',
|
|
297
|
+
* V2: 'v2',
|
|
298
|
+
* });
|
|
299
|
+
*
|
|
300
|
+
* type VersionedData = {
|
|
301
|
+
* [Version.V1]: { count: number };
|
|
302
|
+
* [Version.V2]: { count: number; label: string };
|
|
303
|
+
* };
|
|
304
|
+
*
|
|
305
|
+
* const dataModel = new DataModelBuilder<VersionedData>()
|
|
306
|
+
* .from(Version.V1)
|
|
307
|
+
* .migrate(Version.V2, (data) => ({ ...data, label: '' }))
|
|
308
|
+
* .init(() => ({ count: 0, label: '' }));
|
|
309
|
+
*/
|
|
310
|
+
export class DataModelBuilder<VersionedData extends DataVersionMap> {
|
|
311
|
+
/**
|
|
312
|
+
* Start a migration chain from an initial version.
|
|
313
|
+
*
|
|
314
|
+
* @typeParam InitialVersion - The starting version key (inferred from argument)
|
|
315
|
+
* @param initialVersion - The version key to start from
|
|
316
|
+
* @returns Migration chain builder for adding migrations
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* new DataModelBuilder<VersionedData>()
|
|
320
|
+
* .from(Version.V1)
|
|
321
|
+
* .migrate(Version.V2, (data) => ({ ...data, newField: '' }))
|
|
322
|
+
*/
|
|
323
|
+
from<InitialVersion extends keyof VersionedData & string>(
|
|
324
|
+
initialVersion: InitialVersion,
|
|
325
|
+
): DataModelMigrationChain<VersionedData, InitialVersion, Exclude<keyof VersionedData & string, InitialVersion>> {
|
|
326
|
+
return new DataModelMigrationChain<
|
|
327
|
+
VersionedData,
|
|
328
|
+
InitialVersion,
|
|
329
|
+
Exclude<keyof VersionedData & string, InitialVersion>
|
|
330
|
+
>({ versionChain: [initialVersion] });
|
|
146
331
|
}
|
|
147
332
|
}
|
|
148
333
|
|
|
@@ -150,14 +335,17 @@ class DataModelBuilder<
|
|
|
150
335
|
* DataModel defines the block's data structure, initial values, and migrations.
|
|
151
336
|
* Used by BlockModelV3 to manage data state.
|
|
152
337
|
*
|
|
338
|
+
* Two ways to create a DataModel:
|
|
339
|
+
*
|
|
340
|
+
* 1. **Simple (no migrations)** - Use `DataModel.create()`:
|
|
153
341
|
* @example
|
|
154
|
-
* // Simple data model (no migrations)
|
|
155
342
|
* const dataModel = DataModel.create<BlockData>(() => ({
|
|
156
343
|
* numbers: [],
|
|
157
344
|
* labels: [],
|
|
158
345
|
* }));
|
|
159
346
|
*
|
|
160
|
-
*
|
|
347
|
+
* 2. **With migrations** - Use `new DataModelBuilder<VersionedData>()`:
|
|
348
|
+
* @example
|
|
161
349
|
* const Version = defineDataVersions({
|
|
162
350
|
* V1: 'v1',
|
|
163
351
|
* V2: 'v2',
|
|
@@ -170,8 +358,8 @@ class DataModelBuilder<
|
|
|
170
358
|
* [Version.V3]: { numbers: number[]; labels: string[]; description: string };
|
|
171
359
|
* };
|
|
172
360
|
*
|
|
173
|
-
* const dataModel =
|
|
174
|
-
* .from
|
|
361
|
+
* const dataModel = new DataModelBuilder<VersionedData>()
|
|
362
|
+
* .from(Version.V1)
|
|
175
363
|
* .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
|
|
176
364
|
* .migrate(Version.V3, (data) => ({ ...data, description: '' }))
|
|
177
365
|
* .recover((version, data) => {
|
|
@@ -180,7 +368,7 @@ class DataModelBuilder<
|
|
|
180
368
|
* }
|
|
181
369
|
* return defaultRecover(version, data);
|
|
182
370
|
* })
|
|
183
|
-
* .
|
|
371
|
+
* .init(() => ({ numbers: [], labels: [], description: '' }));
|
|
184
372
|
*/
|
|
185
373
|
export class DataModel<State> {
|
|
186
374
|
private readonly versionChain: DataVersionKey[];
|
|
@@ -188,62 +376,85 @@ export class DataModel<State> {
|
|
|
188
376
|
private readonly initialDataFn: () => State;
|
|
189
377
|
private readonly recoverFn: DataRecoverFn<State>;
|
|
190
378
|
|
|
191
|
-
private constructor(
|
|
192
|
-
versionChain
|
|
193
|
-
steps
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
379
|
+
private constructor({
|
|
380
|
+
versionChain,
|
|
381
|
+
steps,
|
|
382
|
+
initialDataFn,
|
|
383
|
+
recoverFn = defaultRecover as DataRecoverFn<State>,
|
|
384
|
+
}: {
|
|
385
|
+
versionChain: DataVersionKey[];
|
|
386
|
+
steps: MigrationStep[];
|
|
387
|
+
initialDataFn: () => State;
|
|
388
|
+
recoverFn?: DataRecoverFn<State>;
|
|
389
|
+
}) {
|
|
197
390
|
if (versionChain.length === 0) {
|
|
198
391
|
throw new Error('DataModel requires at least one version key');
|
|
199
392
|
}
|
|
200
393
|
this.versionChain = versionChain;
|
|
201
394
|
this.steps = steps;
|
|
202
|
-
this.initialDataFn =
|
|
203
|
-
this.recoverFn =
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Start a migration chain from an initial type */
|
|
207
|
-
static from<
|
|
208
|
-
VersionedData extends DataVersionMap,
|
|
209
|
-
InitialVersion extends keyof VersionedData & string = keyof VersionedData & string,
|
|
210
|
-
>(initialVersion: InitialVersion): DataModelBuilder<VersionedData, InitialVersion> {
|
|
211
|
-
return DataModelBuilder.from<VersionedData, InitialVersion>(initialVersion);
|
|
395
|
+
this.initialDataFn = initialDataFn;
|
|
396
|
+
this.recoverFn = recoverFn;
|
|
212
397
|
}
|
|
213
398
|
|
|
214
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* Create a DataModel with just initial data (no migrations).
|
|
401
|
+
*
|
|
402
|
+
* Use this for simple blocks that don't need version migrations.
|
|
403
|
+
* The version will be set to an internal default value.
|
|
404
|
+
*
|
|
405
|
+
* @typeParam S - The state type
|
|
406
|
+
* @param initialData - Factory function returning initial state
|
|
407
|
+
* @param version - Optional custom version key (defaults to internal version)
|
|
408
|
+
* @returns Finalized DataModel instance
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* const dataModel = DataModel.create<BlockData>(() => ({
|
|
412
|
+
* numbers: [],
|
|
413
|
+
* labels: [],
|
|
414
|
+
* }));
|
|
415
|
+
*/
|
|
215
416
|
static create<S>(initialData: () => S, version: DataVersionKey = DATA_MODEL_DEFAULT_VERSION): DataModel<S> {
|
|
216
|
-
return new DataModel<S>(
|
|
417
|
+
return new DataModel<S>({
|
|
418
|
+
versionChain: [version],
|
|
419
|
+
steps: [],
|
|
420
|
+
initialDataFn: initialData,
|
|
421
|
+
});
|
|
217
422
|
}
|
|
218
423
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return new DataModel<S>(versionChain, steps, initialData, recover);
|
|
424
|
+
/**
|
|
425
|
+
* Internal method for creating DataModel from builder.
|
|
426
|
+
* Uses Symbol key to prevent external access.
|
|
427
|
+
* @internal
|
|
428
|
+
*/
|
|
429
|
+
static [FROM_BUILDER]<S>(state: BuilderState<S>): DataModel<S> {
|
|
430
|
+
return new DataModel<S>(state);
|
|
227
431
|
}
|
|
228
432
|
|
|
229
433
|
/**
|
|
230
|
-
*
|
|
434
|
+
* The latest (current) version key in the migration chain.
|
|
231
435
|
*/
|
|
232
436
|
get version(): DataVersionKey {
|
|
233
437
|
return this.versionChain[this.versionChain.length - 1];
|
|
234
438
|
}
|
|
235
439
|
|
|
236
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* Number of migration steps defined.
|
|
442
|
+
*/
|
|
237
443
|
get migrationCount(): number {
|
|
238
444
|
return this.steps.length;
|
|
239
445
|
}
|
|
240
446
|
|
|
241
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* Get a fresh copy of the initial data.
|
|
449
|
+
*/
|
|
242
450
|
initialData(): State {
|
|
243
451
|
return this.initialDataFn();
|
|
244
452
|
}
|
|
245
453
|
|
|
246
|
-
/**
|
|
454
|
+
/**
|
|
455
|
+
* Get initial data wrapped with current version.
|
|
456
|
+
* Used when creating new blocks or resetting to defaults.
|
|
457
|
+
*/
|
|
247
458
|
getDefaultData(): DataVersioned<State> {
|
|
248
459
|
return makeDataVersioned(this.version, this.initialDataFn());
|
|
249
460
|
}
|
|
@@ -265,8 +476,14 @@ export class DataModel<State> {
|
|
|
265
476
|
|
|
266
477
|
/**
|
|
267
478
|
* Migrate versioned data from any version to the latest.
|
|
268
|
-
*
|
|
269
|
-
* If
|
|
479
|
+
*
|
|
480
|
+
* - If data is already at latest version, returns as-is
|
|
481
|
+
* - If version is in chain, applies needed migrations
|
|
482
|
+
* - If version is unknown, calls recover function
|
|
483
|
+
* - If migration/recovery fails, returns default data with warning
|
|
484
|
+
*
|
|
485
|
+
* @param versioned - Data with version tag
|
|
486
|
+
* @returns Migration result with data at latest version
|
|
270
487
|
*/
|
|
271
488
|
migrate(versioned: DataVersioned<unknown>): DataMigrationResult<State> {
|
|
272
489
|
const { version: fromVersion, data } = versioned;
|
|
@@ -301,14 +518,11 @@ export class DataModel<State> {
|
|
|
301
518
|
* Register callbacks for use in the VM.
|
|
302
519
|
* Called by BlockModelV3.create() to set up internal callbacks.
|
|
303
520
|
*
|
|
304
|
-
*
|
|
305
|
-
* - `__pl_data_initial`: returns initial data for new blocks
|
|
306
|
-
* - `__pl_data_migrate`: migrates versioned data from any version to latest
|
|
307
|
-
* - `__pl_storage_initial`: returns initial BlockStorage as JSON string
|
|
521
|
+
* @internal
|
|
308
522
|
*/
|
|
309
523
|
registerCallbacks(): void {
|
|
310
524
|
tryRegisterCallback('__pl_data_initial', () => this.initialDataFn());
|
|
311
|
-
tryRegisterCallback('
|
|
525
|
+
tryRegisterCallback('__pl_data_upgrade', (versioned: DataVersioned<unknown>) => this.migrate(versioned));
|
|
312
526
|
tryRegisterCallback('__pl_storage_initial', () => {
|
|
313
527
|
const { version, data } = this.getDefaultData();
|
|
314
528
|
const storage = createBlockStorage(data, version);
|
package/src/block_storage.ts
CHANGED
|
@@ -105,7 +105,13 @@ export function createBlockStorage<TState = unknown>(
|
|
|
105
105
|
export function normalizeBlockStorage<TState = unknown>(raw: unknown): BlockStorage<TState> {
|
|
106
106
|
if (isBlockStorage(raw)) {
|
|
107
107
|
const storage = raw as BlockStorage<TState>;
|
|
108
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
...storage,
|
|
110
|
+
// Fix for early released version where __dataVersion was a number
|
|
111
|
+
__dataVersion: typeof storage.__dataVersion === 'number'
|
|
112
|
+
? DATA_MODEL_DEFAULT_VERSION
|
|
113
|
+
: storage.__dataVersion,
|
|
114
|
+
};
|
|
109
115
|
}
|
|
110
116
|
// Legacy format: raw is the state directly
|
|
111
117
|
return createBlockStorage(raw as TState);
|
package/src/block_storage_vm.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Callbacks registered by DataModel.registerCallbacks():
|
|
16
16
|
* - `__pl_data_initial`: () => initial data
|
|
17
|
-
* - `
|
|
17
|
+
* - `__pl_data_upgrade`: (versioned) => DataMigrationResult
|
|
18
18
|
* - `__pl_storage_initial`: () => initial BlockStorage as JSON string
|
|
19
19
|
*
|
|
20
20
|
* @module block_storage_vm
|
|
@@ -178,7 +178,7 @@ interface DataMigrationResult {
|
|
|
178
178
|
* Runs storage migration using the DataModel's migrate callback.
|
|
179
179
|
* This is the main entry point for the middle layer to trigger migrations.
|
|
180
180
|
*
|
|
181
|
-
* Uses the '
|
|
181
|
+
* Uses the '__pl_data_upgrade' callback registered by DataModel.registerCallbacks() which:
|
|
182
182
|
* - Handles all migration logic internally
|
|
183
183
|
* - Returns { version, data, warning? } - warning present if reset to initial data
|
|
184
184
|
*
|
|
@@ -206,9 +206,9 @@ function migrateStorage(currentStorageJson: string | undefined): MigrationResult
|
|
|
206
206
|
};
|
|
207
207
|
|
|
208
208
|
// Get the migrate callback (registered by DataModel.registerCallbacks())
|
|
209
|
-
const migrateCallback = ctx.callbackRegistry['
|
|
209
|
+
const migrateCallback = ctx.callbackRegistry['__pl_data_upgrade'] as ((v: { version: string; data: unknown }) => DataMigrationResult) | undefined;
|
|
210
210
|
if (typeof migrateCallback !== 'function') {
|
|
211
|
-
return { error: '
|
|
211
|
+
return { error: '__pl_data_upgrade callback not found (DataModel not registered)' };
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// Call the migrator's migrate function
|