@platforma-sdk/model 1.57.2 → 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 +18 -14
- package/dist/block_model.cjs.map +1 -1
- package/dist/block_model.d.ts +5 -5
- package/dist/block_model.d.ts.map +1 -1
- package/dist/block_model.js +18 -14
- package/dist/block_model.js.map +1 -1
- package/dist/block_model_legacy.cjs +1 -0
- package/dist/block_model_legacy.cjs.map +1 -1
- package/dist/block_model_legacy.d.ts.map +1 -1
- package/dist/block_model_legacy.js +1 -0
- package/dist/block_model_legacy.js.map +1 -1
- package/dist/block_storage.cjs +24 -20
- package/dist/block_storage.cjs.map +1 -1
- package/dist/block_storage.d.ts +20 -16
- package/dist/block_storage.d.ts.map +1 -1
- package/dist/block_storage.js +24 -20
- package/dist/block_storage.js.map +1 -1
- package/dist/block_storage_callbacks.cjs +4 -3
- package/dist/block_storage_callbacks.cjs.map +1 -1
- package/dist/block_storage_callbacks.d.ts +7 -5
- package/dist/block_storage_callbacks.d.ts.map +1 -1
- package/dist/block_storage_callbacks.js +4 -3
- package/dist/block_storage_callbacks.js.map +1 -1
- package/dist/components/PFrameForGraphs.cjs +0 -117
- package/dist/components/PFrameForGraphs.cjs.map +1 -1
- package/dist/components/PFrameForGraphs.d.ts +3 -5
- package/dist/components/PFrameForGraphs.d.ts.map +1 -1
- package/dist/components/PFrameForGraphs.js +2 -117
- package/dist/components/PFrameForGraphs.js.map +1 -1
- package/dist/index.cjs +8 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/pframe_utils/axes.cjs +131 -0
- package/dist/pframe_utils/axes.cjs.map +1 -0
- package/dist/pframe_utils/axes.d.ts +15 -0
- package/dist/pframe_utils/axes.d.ts.map +1 -0
- package/dist/pframe_utils/axes.js +128 -0
- package/dist/pframe_utils/axes.js.map +1 -0
- package/dist/pframe_utils/columns.cjs +4 -8
- package/dist/pframe_utils/columns.cjs.map +1 -1
- package/dist/pframe_utils/columns.js +1 -5
- package/dist/pframe_utils/columns.js.map +1 -1
- package/dist/pframe_utils/index.cjs +0 -3
- package/dist/pframe_utils/index.cjs.map +1 -1
- package/dist/pframe_utils/index.js +0 -3
- package/dist/pframe_utils/index.js.map +1 -1
- package/dist/platforma.d.ts +12 -2
- package/dist/platforma.d.ts.map +1 -1
- package/dist/plugin_handle.cjs +29 -0
- package/dist/plugin_handle.cjs.map +1 -0
- package/dist/plugin_handle.d.ts +51 -0
- package/dist/plugin_handle.d.ts.map +1 -0
- package/dist/plugin_handle.js +25 -0
- package/dist/plugin_handle.js.map +1 -0
- package/dist/plugin_model.cjs +29 -29
- package/dist/plugin_model.cjs.map +1 -1
- package/dist/plugin_model.d.ts +43 -35
- package/dist/plugin_model.d.ts.map +1 -1
- package/dist/plugin_model.js +29 -29
- package/dist/plugin_model.js.map +1 -1
- package/dist/render/api.cjs +9 -5
- package/dist/render/api.cjs.map +1 -1
- package/dist/render/api.d.ts +11 -5
- package/dist/render/api.d.ts.map +1 -1
- package/dist/render/api.js +9 -5
- package/dist/render/api.js.map +1 -1
- package/package.json +6 -6
- package/src/block_migrations.test.ts +109 -12
- package/src/block_migrations.ts +63 -87
- package/src/block_model.ts +34 -20
- package/src/block_model_legacy.ts +1 -0
- package/src/block_storage.test.ts +11 -10
- package/src/block_storage.ts +40 -32
- package/src/block_storage_callbacks.ts +12 -10
- package/src/components/PFrameForGraphs.ts +4 -167
- package/src/index.ts +24 -2
- package/src/pframe_utils/axes.ts +175 -0
- package/src/pframe_utils/columns.ts +2 -2
- package/src/platforma.ts +17 -2
- package/src/plugin_handle.ts +85 -0
- package/src/plugin_model.test.ts +2 -2
- package/src/plugin_model.ts +120 -58
- package/src/render/api.ts +21 -11
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { DataModelBuilder, defaultRecover, makeDataVersioned } from "./block_migrations";
|
|
3
|
+
import { DATA_MODEL_LEGACY_VERSION } from "./block_storage";
|
|
3
4
|
|
|
4
5
|
describe("makeDataVersioned", () => {
|
|
5
6
|
it("creates correct DataVersioned shape", () => {
|
|
@@ -18,7 +19,6 @@ describe("DataModel migrations", () => {
|
|
|
18
19
|
const result = dataModel.migrate(makeDataVersioned("legacy", { count: 42 }));
|
|
19
20
|
expect(result.version).toBe("v2");
|
|
20
21
|
expect(result.data).toStrictEqual({ count: 0, label: "" });
|
|
21
|
-
expect(result.warning).toBe(`Unknown version 'legacy'`);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("throws at build time on duplicate version key", () => {
|
|
@@ -33,7 +33,7 @@ describe("DataModel migrations", () => {
|
|
|
33
33
|
).toThrow("Duplicate version 'v1' in migration chain");
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it("
|
|
36
|
+
it("throws on migration failure", () => {
|
|
37
37
|
const dataModel = new DataModelBuilder()
|
|
38
38
|
.from<{ numbers: number[] }>("v1")
|
|
39
39
|
.migrate<{ numbers: number[]; label: string }>("v2", (v1) => {
|
|
@@ -42,10 +42,9 @@ describe("DataModel migrations", () => {
|
|
|
42
42
|
})
|
|
43
43
|
.init(() => ({ numbers: [], label: "" }));
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
expect(result.warning).toBe(`Migration v1→v2 failed: Forbidden number`);
|
|
45
|
+
expect(() => dataModel.migrate(makeDataVersioned("v1", { numbers: [666] }))).toThrow(
|
|
46
|
+
"Forbidden number",
|
|
47
|
+
);
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
describe("recover()", () => {
|
|
@@ -70,7 +69,6 @@ describe("DataModel migrations", () => {
|
|
|
70
69
|
const result = dataModel.migrate(makeDataVersioned("legacy", { count: 5 }));
|
|
71
70
|
expect(result.version).toBe("v2");
|
|
72
71
|
expect(result.data).toStrictEqual({ count: 5, label: "default" });
|
|
73
|
-
expect(result.warning).toBeUndefined();
|
|
74
72
|
});
|
|
75
73
|
|
|
76
74
|
it("recover() between migrations — recovered data goes through subsequent migrations", () => {
|
|
@@ -97,7 +95,6 @@ describe("DataModel migrations", () => {
|
|
|
97
95
|
const result = dataModel.migrate(makeDataVersioned("legacy", { count: 7 }));
|
|
98
96
|
expect(result.version).toBe("v3");
|
|
99
97
|
expect(result.data).toStrictEqual({ count: 7, label: "recovered", description: "added" });
|
|
100
|
-
expect(result.warning).toBeUndefined();
|
|
101
98
|
});
|
|
102
99
|
|
|
103
100
|
it("recover() at the end of chain — recovered data is the final type", () => {
|
|
@@ -122,10 +119,9 @@ describe("DataModel migrations", () => {
|
|
|
122
119
|
const result = dataModel.migrate(makeDataVersioned("legacy", { count: 9 }));
|
|
123
120
|
expect(result.version).toBe("v2");
|
|
124
121
|
expect(result.data).toStrictEqual({ count: 9, label: "recovered" });
|
|
125
|
-
expect(result.warning).toBeUndefined();
|
|
126
122
|
});
|
|
127
123
|
|
|
128
|
-
it("recover() delegates to defaultRecover for truly unknown versions", () => {
|
|
124
|
+
it("recover() delegates to defaultRecover for truly unknown versions — resets to initial data", () => {
|
|
129
125
|
const dataModel = new DataModelBuilder()
|
|
130
126
|
.from<{ count: number }>("v1")
|
|
131
127
|
.migrate<{ count: number; label: string }>("v2", (v1) => ({ ...v1, label: "" }))
|
|
@@ -135,7 +131,6 @@ describe("DataModel migrations", () => {
|
|
|
135
131
|
const result = dataModel.migrate(makeDataVersioned("unknown", { count: 7 }));
|
|
136
132
|
expect(result.version).toBe("v2");
|
|
137
133
|
expect(result.data).toStrictEqual({ count: 0, label: "" });
|
|
138
|
-
expect(result.warning).toBe(`Unknown version 'unknown'`);
|
|
139
134
|
});
|
|
140
135
|
|
|
141
136
|
it("migration failure after recover() resets to initial data", () => {
|
|
@@ -164,7 +159,6 @@ describe("DataModel migrations", () => {
|
|
|
164
159
|
const result = dataModel.migrate(makeDataVersioned("legacy", { count: 7 }));
|
|
165
160
|
expect(result.version).toBe("v3");
|
|
166
161
|
expect(result.data).toStrictEqual({ count: 0, label: "", description: "" });
|
|
167
|
-
expect(result.warning).toBe("Migration v2→v3 failed: v3 failed");
|
|
168
162
|
});
|
|
169
163
|
|
|
170
164
|
it("recover() cannot be called twice — enforced by type (no recover() on WithRecover)", () => {
|
|
@@ -172,4 +166,107 @@ describe("DataModel migrations", () => {
|
|
|
172
166
|
// Verified by the absence of recover() in DataModelMigrationChainWithRecover.
|
|
173
167
|
});
|
|
174
168
|
});
|
|
169
|
+
|
|
170
|
+
describe("upgradeLegacy()", () => {
|
|
171
|
+
type LegacyArgs = { inputFile: string; threshold: number };
|
|
172
|
+
type LegacyUiState = { selectedTab: string };
|
|
173
|
+
type BlockData = { inputFile: string; threshold: number; selectedTab: string };
|
|
174
|
+
|
|
175
|
+
it("upgrades legacy { args, uiState } data with custom initial version", () => {
|
|
176
|
+
const dataModel = new DataModelBuilder()
|
|
177
|
+
.from<BlockData>("v1")
|
|
178
|
+
.upgradeLegacy<LegacyArgs, LegacyUiState>(({ args, uiState }) => ({
|
|
179
|
+
inputFile: args.inputFile,
|
|
180
|
+
threshold: args.threshold,
|
|
181
|
+
selectedTab: uiState.selectedTab,
|
|
182
|
+
}))
|
|
183
|
+
.init(() => ({ inputFile: "", threshold: 0, selectedTab: "main" }));
|
|
184
|
+
|
|
185
|
+
// Legacy data arrives at DATA_MODEL_LEGACY_VERSION (how normalizeBlockStorage wraps it)
|
|
186
|
+
const result = dataModel.migrate(
|
|
187
|
+
makeDataVersioned(DATA_MODEL_LEGACY_VERSION, {
|
|
188
|
+
args: { inputFile: "test.fa", threshold: 5 },
|
|
189
|
+
uiState: { selectedTab: "results" },
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
expect(result.version).toBe("v1");
|
|
193
|
+
expect(result.data).toStrictEqual({
|
|
194
|
+
inputFile: "test.fa",
|
|
195
|
+
threshold: 5,
|
|
196
|
+
selectedTab: "results",
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("passes through non-legacy data at custom initial version unchanged", () => {
|
|
201
|
+
const dataModel = new DataModelBuilder()
|
|
202
|
+
.from<BlockData>("v1")
|
|
203
|
+
.upgradeLegacy<LegacyArgs, LegacyUiState>(({ args, uiState }) => ({
|
|
204
|
+
inputFile: args.inputFile,
|
|
205
|
+
threshold: args.threshold,
|
|
206
|
+
selectedTab: uiState.selectedTab,
|
|
207
|
+
}))
|
|
208
|
+
.init(() => ({ inputFile: "", threshold: 0, selectedTab: "main" }));
|
|
209
|
+
|
|
210
|
+
// Non-legacy data at the user's version passes through unchanged
|
|
211
|
+
const result = dataModel.migrate(
|
|
212
|
+
makeDataVersioned("v1", {
|
|
213
|
+
inputFile: "existing.fa",
|
|
214
|
+
threshold: 10,
|
|
215
|
+
selectedTab: "overview",
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
expect(result.version).toBe("v1");
|
|
219
|
+
expect(result.data).toStrictEqual({
|
|
220
|
+
inputFile: "existing.fa",
|
|
221
|
+
threshold: 10,
|
|
222
|
+
selectedTab: "overview",
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("upgrades legacy data and runs subsequent migrations", () => {
|
|
227
|
+
type BlockDataV2 = BlockData & { description: string };
|
|
228
|
+
|
|
229
|
+
const dataModel = new DataModelBuilder()
|
|
230
|
+
.from<BlockData>("v1")
|
|
231
|
+
.upgradeLegacy<LegacyArgs, LegacyUiState>(({ args, uiState }) => ({
|
|
232
|
+
inputFile: args.inputFile,
|
|
233
|
+
threshold: args.threshold,
|
|
234
|
+
selectedTab: uiState.selectedTab,
|
|
235
|
+
}))
|
|
236
|
+
.migrate<BlockDataV2>("v2", (v1) => ({ ...v1, description: "auto" }))
|
|
237
|
+
.init(() => ({ inputFile: "", threshold: 0, selectedTab: "main", description: "" }));
|
|
238
|
+
|
|
239
|
+
const result = dataModel.migrate(
|
|
240
|
+
makeDataVersioned(DATA_MODEL_LEGACY_VERSION, {
|
|
241
|
+
args: { inputFile: "test.fa", threshold: 3 },
|
|
242
|
+
uiState: { selectedTab: "tab1" },
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
expect(result.version).toBe("v2");
|
|
246
|
+
expect(result.data).toStrictEqual({
|
|
247
|
+
inputFile: "test.fa",
|
|
248
|
+
threshold: 3,
|
|
249
|
+
selectedTab: "tab1",
|
|
250
|
+
description: "auto",
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("throws when legacy upgrade fails", () => {
|
|
255
|
+
const dataModel = new DataModelBuilder()
|
|
256
|
+
.from<BlockData>("v1")
|
|
257
|
+
.upgradeLegacy<LegacyArgs, LegacyUiState>(() => {
|
|
258
|
+
throw new Error("bad legacy data");
|
|
259
|
+
})
|
|
260
|
+
.init(() => ({ inputFile: "", threshold: 0, selectedTab: "main" }));
|
|
261
|
+
|
|
262
|
+
expect(() =>
|
|
263
|
+
dataModel.migrate(
|
|
264
|
+
makeDataVersioned(DATA_MODEL_LEGACY_VERSION, {
|
|
265
|
+
args: { inputFile: "test.fa", threshold: 5 },
|
|
266
|
+
uiState: { selectedTab: "results" },
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
269
|
+
).toThrow("bad legacy data");
|
|
270
|
+
});
|
|
271
|
+
});
|
|
175
272
|
});
|
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
|
@@ -6,12 +6,14 @@ import type {
|
|
|
6
6
|
BlockCodeKnownFeatureFlags,
|
|
7
7
|
BlockConfigContainer,
|
|
8
8
|
} from "@milaboratories/pl-model-common";
|
|
9
|
+
import { mergeFeatureFlags } from "@milaboratories/pl-model-common";
|
|
9
10
|
import { getPlatformaInstance, isInUI, createAndRegisterRenderLambda } from "./internal";
|
|
10
11
|
import type { DataModel } from "./block_migrations";
|
|
11
12
|
import type { PlatformaV3 } from "./platforma";
|
|
12
13
|
import type { InferRenderFunctionReturn, RenderFunction } from "./render";
|
|
13
14
|
import { BlockRenderCtx, PluginRenderCtx } from "./render";
|
|
14
|
-
import type { PluginModel } from "./plugin_model";
|
|
15
|
+
import type { PluginData, PluginModel, PluginOutputs, PluginParams } from "./plugin_model";
|
|
16
|
+
import { type PluginHandle, pluginOutputKey } from "./plugin_handle";
|
|
15
17
|
import type { RenderCtxBase } from "./render";
|
|
16
18
|
import { PlatformaSDKVersion } from "./version";
|
|
17
19
|
import {
|
|
@@ -59,7 +61,11 @@ type ParamsInputErased = Record<string, (ctx: RenderCtxBase) => unknown>;
|
|
|
59
61
|
* Registered plugin: model + param derivation lambdas.
|
|
60
62
|
* Type parameters are carried by PluginModel generic.
|
|
61
63
|
*/
|
|
62
|
-
export type PluginInstance<
|
|
64
|
+
export type PluginInstance<
|
|
65
|
+
Data extends PluginData = PluginData,
|
|
66
|
+
Params extends PluginParams = undefined,
|
|
67
|
+
Outputs extends PluginOutputs = PluginOutputs,
|
|
68
|
+
> = {
|
|
63
69
|
readonly model: PluginModel<Data, Params, Outputs>;
|
|
64
70
|
readonly inputs: ParamsInputErased;
|
|
65
71
|
};
|
|
@@ -108,7 +114,7 @@ export class BlockModelV3<
|
|
|
108
114
|
*
|
|
109
115
|
* @example
|
|
110
116
|
* const dataModel = new DataModelBuilder()
|
|
111
|
-
* .from<BlockData>(
|
|
117
|
+
* .from<BlockData>("v1")
|
|
112
118
|
* .init(() => ({ numbers: [], labels: [] }));
|
|
113
119
|
*
|
|
114
120
|
* BlockModelV3.create(dataModel)
|
|
@@ -125,7 +131,6 @@ export class BlockModelV3<
|
|
|
125
131
|
renderingMode: "Heavy",
|
|
126
132
|
dataModel,
|
|
127
133
|
outputs: {},
|
|
128
|
-
// Register default sections callback (returns empty array)
|
|
129
134
|
sections: createAndRegisterRenderLambda({ handle: "sections", lambda: () => [] }, true),
|
|
130
135
|
title: undefined,
|
|
131
136
|
subtitle: undefined,
|
|
@@ -372,23 +377,28 @@ export class BlockModelV3<
|
|
|
372
377
|
* sourceId: (ctx) => ctx.data.selectedSource,
|
|
373
378
|
* })
|
|
374
379
|
*/
|
|
375
|
-
public plugin<
|
|
380
|
+
public plugin<
|
|
381
|
+
const PluginId extends string,
|
|
382
|
+
PData extends PluginData,
|
|
383
|
+
PParams extends PluginParams,
|
|
384
|
+
POutputs extends PluginOutputs,
|
|
385
|
+
>(
|
|
376
386
|
pluginId: PluginId,
|
|
377
|
-
plugin: PluginModel<
|
|
378
|
-
params?: ParamsInput<
|
|
387
|
+
plugin: PluginModel<PData, PParams, POutputs>,
|
|
388
|
+
params?: ParamsInput<PParams, Args, Data>,
|
|
379
389
|
): BlockModelV3<
|
|
380
390
|
Args,
|
|
381
391
|
OutputsCfg,
|
|
382
392
|
Data,
|
|
383
393
|
Href,
|
|
384
|
-
Plugins & { [K in PluginId]: PluginInstance<
|
|
394
|
+
Plugins & { [K in PluginId]: PluginInstance<PData, PParams, POutputs> }
|
|
385
395
|
> {
|
|
386
396
|
// Validate pluginId uniqueness
|
|
387
397
|
if (pluginId in this.config.plugins) {
|
|
388
398
|
throw new Error(`Plugin '${pluginId}' already registered`);
|
|
389
399
|
}
|
|
390
400
|
|
|
391
|
-
const instance: PluginInstance<
|
|
401
|
+
const instance: PluginInstance<PData, PParams, POutputs> = {
|
|
392
402
|
model: plugin,
|
|
393
403
|
inputs: (params ?? {}) as ParamsInputErased,
|
|
394
404
|
};
|
|
@@ -399,6 +409,7 @@ export class BlockModelV3<
|
|
|
399
409
|
...this.config.plugins,
|
|
400
410
|
[pluginId]: instance,
|
|
401
411
|
},
|
|
412
|
+
featureFlags: mergeFeatureFlags(this.config.featureFlags, plugin.featureFlags ?? {}),
|
|
402
413
|
});
|
|
403
414
|
}
|
|
404
415
|
|
|
@@ -423,15 +434,16 @@ export class BlockModelV3<
|
|
|
423
434
|
// Build plugin registry
|
|
424
435
|
const { plugins } = this.config;
|
|
425
436
|
const pluginRegistry: Record<string, PluginName> = {};
|
|
426
|
-
|
|
427
|
-
|
|
437
|
+
const pluginHandles = Object.keys(plugins) as PluginHandle[];
|
|
438
|
+
for (const handle of pluginHandles) {
|
|
439
|
+
pluginRegistry[handle] = plugins[handle].model.name;
|
|
428
440
|
}
|
|
429
441
|
|
|
430
442
|
const { dataModel, argsFunction, prerunArgsFunction } = this.config;
|
|
431
443
|
|
|
432
|
-
function getPlugin(
|
|
433
|
-
const plugin = plugins[
|
|
434
|
-
if (!plugin) throw new Error(`Plugin model not found for '${
|
|
444
|
+
function getPlugin(handle: PluginHandle): PluginInstance {
|
|
445
|
+
const plugin = plugins[handle];
|
|
446
|
+
if (!plugin) throw new Error(`Plugin model not found for '${handle}'`);
|
|
435
447
|
return plugin;
|
|
436
448
|
}
|
|
437
449
|
|
|
@@ -443,14 +455,14 @@ export class BlockModelV3<
|
|
|
443
455
|
migrateStorage(currentStorageJson, {
|
|
444
456
|
migrateBlockData: (v) => dataModel.migrate(v),
|
|
445
457
|
getPluginRegistry: () => pluginRegistry,
|
|
446
|
-
migratePluginData: (
|
|
447
|
-
createPluginData: (
|
|
458
|
+
migratePluginData: (handle, v) => getPlugin(handle).model.dataModel.migrate(v),
|
|
459
|
+
createPluginData: (handle) => getPlugin(handle).model.dataModel.getDefaultData(),
|
|
448
460
|
}),
|
|
449
461
|
[BlockStorageFacadeCallbacks.StorageInitial]: () =>
|
|
450
462
|
createInitialStorage({
|
|
451
463
|
getDefaultBlockData: () => dataModel.getDefaultData(),
|
|
452
464
|
getPluginRegistry: () => pluginRegistry,
|
|
453
|
-
createPluginData: (
|
|
465
|
+
createPluginData: (handle) => getPlugin(handle).model.dataModel.getDefaultData(),
|
|
454
466
|
}),
|
|
455
467
|
[BlockStorageFacadeCallbacks.ArgsDerive]: (storageJson) =>
|
|
456
468
|
deriveArgsFromStorage(storageJson, argsFunction),
|
|
@@ -460,7 +472,8 @@ export class BlockModelV3<
|
|
|
460
472
|
|
|
461
473
|
// Register plugin input and output lambdas
|
|
462
474
|
const pluginOutputs: Record<string, ConfigRenderLambda> = {};
|
|
463
|
-
for (const
|
|
475
|
+
for (const handle of pluginHandles) {
|
|
476
|
+
const { model, inputs } = plugins[handle];
|
|
464
477
|
// Wrap plugin param lambdas: close over BlockRenderCtx creation
|
|
465
478
|
const wrappedInputs: Record<string, () => unknown> = {};
|
|
466
479
|
for (const [paramKey, paramFn] of Object.entries(inputs)) {
|
|
@@ -470,10 +483,10 @@ export class BlockModelV3<
|
|
|
470
483
|
// Register plugin outputs (in config pack, evaluated by middle layer)
|
|
471
484
|
const outputs = model.outputs as Record<string, (ctx: PluginRenderCtx) => unknown>;
|
|
472
485
|
for (const [outputKey, outputFn] of Object.entries(outputs)) {
|
|
473
|
-
const key =
|
|
486
|
+
const key = pluginOutputKey(handle, outputKey);
|
|
474
487
|
pluginOutputs[key] = createAndRegisterRenderLambda({
|
|
475
488
|
handle: key,
|
|
476
|
-
lambda: () => outputFn(new PluginRenderCtx(
|
|
489
|
+
lambda: () => outputFn(new PluginRenderCtx(handle, wrappedInputs)),
|
|
477
490
|
});
|
|
478
491
|
}
|
|
479
492
|
}
|
|
@@ -528,6 +541,7 @@ export class BlockModelV3<
|
|
|
528
541
|
},
|
|
529
542
|
]),
|
|
530
543
|
),
|
|
544
|
+
pluginIds: pluginHandles,
|
|
531
545
|
},
|
|
532
546
|
} as any;
|
|
533
547
|
}
|