@platforma-sdk/model 1.53.0 → 1.53.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.
@@ -3,23 +3,85 @@
3
3
  var internal = require('./internal.cjs');
4
4
  var block_storage = require('./block_storage.cjs');
5
5
 
6
+ /**
7
+ * Helper to define version keys with literal type inference and runtime validation.
8
+ * - Validates that all version values are unique
9
+ * - Eliminates need for `as const` assertion
10
+ *
11
+ * @throws Error if duplicate version values are found
12
+ *
13
+ * @example
14
+ * const Version = defineDataVersions({
15
+ * Initial: 'v1',
16
+ * AddedLabels: 'v2',
17
+ * });
18
+ *
19
+ * type VersionedData = {
20
+ * [Version.Initial]: DataV1;
21
+ * [Version.AddedLabels]: DataV2;
22
+ * };
23
+ */
24
+ function defineDataVersions(versions) {
25
+ const values = Object.values(versions);
26
+ const emptyKeys = Object.keys(versions).filter((key) => versions[key] === '');
27
+ if (emptyKeys.length > 0) {
28
+ throw new Error(`Version values must be non-empty strings (empty: ${emptyKeys.join(', ')})`);
29
+ }
30
+ const unique = new Set(values);
31
+ if (unique.size !== values.length) {
32
+ const duplicates = values.filter((v, i) => values.indexOf(v) !== i);
33
+ throw new Error(`Duplicate version values: ${[...new Set(duplicates)].join(', ')}`);
34
+ }
35
+ return versions;
36
+ }
37
+ /** Create a DataVersioned wrapper with correct shape */
38
+ function makeDataVersioned(version, data) {
39
+ return { version, data };
40
+ }
41
+ /** Thrown by recover() to signal unrecoverable data. */
42
+ class DataUnrecoverableError extends Error {
43
+ name = 'DataUnrecoverableError';
44
+ constructor(dataVersion) {
45
+ super(`Unknown version '${dataVersion}'`);
46
+ }
47
+ }
48
+ function isDataUnrecoverableError(error) {
49
+ return error instanceof Error && error.name === 'DataUnrecoverableError';
50
+ }
51
+ /** Default recover function for unknown versions */
52
+ const defaultRecover = (version, _data) => {
53
+ throw new DataUnrecoverableError(version);
54
+ };
6
55
  /** Internal builder for chaining migrations */
7
56
  class DataModelBuilder {
57
+ versionChain;
8
58
  migrationSteps;
9
- constructor(steps = []) {
59
+ recoverFn;
60
+ constructor(versionChain, steps = [], recoverFn) {
61
+ this.versionChain = versionChain;
10
62
  this.migrationSteps = steps;
63
+ this.recoverFn = recoverFn;
11
64
  }
12
- /** Start a migration chain from an initial type */
13
- static from() {
14
- return new DataModelBuilder();
65
+ /** Start a migration chain from an initial version */
66
+ static from(initialVersion) {
67
+ return new DataModelBuilder([initialVersion]);
68
+ }
69
+ /** Add a migration step to the target version */
70
+ migrate(nextVersion, fn) {
71
+ if (this.versionChain.includes(nextVersion)) {
72
+ throw new Error(`Duplicate version '${nextVersion}' in migration chain`);
73
+ }
74
+ const fromVersion = this.versionChain[this.versionChain.length - 1];
75
+ const step = { fromVersion, toVersion: nextVersion, migrate: fn };
76
+ return new DataModelBuilder([...this.versionChain, nextVersion], [...this.migrationSteps, step]);
15
77
  }
16
- /** Add a migration step */
17
- migrate(fn) {
18
- return new DataModelBuilder([...this.migrationSteps, fn]);
78
+ /** Set recovery handler for unknown or unsupported versions */
79
+ recover(fn) {
80
+ return new DataModelBuilder([...this.versionChain], [...this.migrationSteps], fn);
19
81
  }
20
82
  /** Finalize with initial data, creating the DataModel */
21
83
  create(initialData, ..._) {
22
- return DataModel._fromBuilder(this.migrationSteps, initialData);
84
+ return DataModel._fromBuilder(this.versionChain, this.migrationSteps, initialData, this.recoverFn);
23
85
  }
24
86
  }
25
87
  /**
@@ -34,37 +96,61 @@ class DataModelBuilder {
34
96
  * }));
35
97
  *
36
98
  * // Data model with migrations
99
+ * const Version = defineDataVersions({
100
+ * V1: 'v1',
101
+ * V2: 'v2',
102
+ * V3: 'v3',
103
+ * });
104
+ *
105
+ * type VersionedData = {
106
+ * [Version.V1]: { numbers: number[] };
107
+ * [Version.V2]: { numbers: number[]; labels: string[] };
108
+ * [Version.V3]: { numbers: number[]; labels: string[]; description: string };
109
+ * };
110
+ *
37
111
  * const dataModel = DataModel
38
- * .from<V1>()
39
- * .migrate((data) => ({ ...data, labels: [] })) // v1 → v2
40
- * .migrate((data) => ({ ...data, description: '' })) // v2 → v3
41
- * .create<BlockData>(() => ({ numbers: [], labels: [], description: '' }));
112
+ * .from<VersionedData>(Version.V1)
113
+ * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
114
+ * .migrate(Version.V3, (data) => ({ ...data, description: '' }))
115
+ * .recover((version, data) => {
116
+ * if (version === 'legacy' && typeof data === 'object' && data !== null && 'numbers' in data) {
117
+ * return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };
118
+ * }
119
+ * return defaultRecover(version, data);
120
+ * })
121
+ * .create(() => ({ numbers: [], labels: [], description: '' }));
42
122
  */
43
123
  class DataModel {
124
+ versionChain;
44
125
  steps;
45
- _initialData;
46
- constructor(steps, initialData) {
126
+ initialDataFn;
127
+ recoverFn;
128
+ constructor(versionChain, steps, initialData, recover = defaultRecover) {
129
+ if (versionChain.length === 0) {
130
+ throw new Error('DataModel requires at least one version key');
131
+ }
132
+ this.versionChain = versionChain;
47
133
  this.steps = steps;
48
- this._initialData = initialData;
134
+ this.initialDataFn = initialData;
135
+ this.recoverFn = recover;
49
136
  }
50
137
  /** Start a migration chain from an initial type */
51
- static from() {
52
- return DataModelBuilder.from();
138
+ static from(initialVersion) {
139
+ return DataModelBuilder.from(initialVersion);
53
140
  }
54
141
  /** Create a data model with just initial data (no migrations) */
55
- static create(initialData) {
56
- return new DataModel([], initialData);
142
+ static create(initialData, version = block_storage.DATA_MODEL_DEFAULT_VERSION) {
143
+ return new DataModel([version], [], initialData);
57
144
  }
58
145
  /** Create from builder (internal use) */
59
- static _fromBuilder(steps, initialData) {
60
- return new DataModel(steps, initialData);
146
+ static _fromBuilder(versionChain, steps, initialData, recover) {
147
+ return new DataModel(versionChain, steps, initialData, recover);
61
148
  }
62
149
  /**
63
- * Latest version number.
64
- * Version 1 = initial state, each migration adds 1.
150
+ * Latest version key.
65
151
  */
66
152
  get version() {
67
- return this.steps.length + 1;
153
+ return this.versionChain[this.versionChain.length - 1];
68
154
  }
69
155
  /** Number of migration steps */
70
156
  get migrationCount() {
@@ -72,43 +158,52 @@ class DataModel {
72
158
  }
73
159
  /** Get initial data */
74
160
  initialData() {
75
- return this._initialData();
161
+ return this.initialDataFn();
76
162
  }
77
163
  /** Get default data wrapped with current version */
78
164
  getDefaultData() {
79
- return { version: this.version, data: this._initialData() };
165
+ return makeDataVersioned(this.version, this.initialDataFn());
166
+ }
167
+ recoverFrom(data, version) {
168
+ try {
169
+ return { version: this.version, data: this.recoverFn(version, data) };
170
+ }
171
+ catch (error) {
172
+ if (isDataUnrecoverableError(error)) {
173
+ return { ...this.getDefaultData(), warning: error.message };
174
+ }
175
+ const errorMessage = error instanceof Error ? error.message : String(error);
176
+ return {
177
+ ...this.getDefaultData(),
178
+ warning: `Recover failed for version '${version}': ${errorMessage}`,
179
+ };
180
+ }
80
181
  }
81
182
  /**
82
- * Upgrade versioned data from any version to the latest.
183
+ * Migrate versioned data from any version to the latest.
83
184
  * Applies only the migrations needed (skips already-applied ones).
84
185
  * If a migration fails, returns default data with a warning.
85
186
  */
86
- upgrade(versioned) {
187
+ migrate(versioned) {
87
188
  const { version: fromVersion, data } = versioned;
88
- if (fromVersion > this.version) {
89
- throw new Error(`Cannot downgrade from version ${fromVersion} to ${this.version}`);
90
- }
91
189
  if (fromVersion === this.version) {
92
190
  return { version: this.version, data: data };
93
191
  }
94
- // Apply migrations starting from (fromVersion - 1) index
95
- // Version 1 -> no migrations applied yet -> start at index 0
96
- // Version 2 -> migration[0] already applied -> start at index 1
97
- const startIndex = fromVersion - 1;
98
- const migrationsToApply = this.steps.slice(startIndex);
192
+ const startIndex = this.versionChain.indexOf(fromVersion);
193
+ if (startIndex < 0) {
194
+ return this.recoverFrom(data, fromVersion);
195
+ }
99
196
  let currentData = data;
100
- for (let i = 0; i < migrationsToApply.length; i++) {
101
- const stepIndex = startIndex + i;
102
- const fromVer = stepIndex + 1;
103
- const toVer = stepIndex + 2;
197
+ for (let i = startIndex; i < this.steps.length; i++) {
198
+ const step = this.steps[i];
104
199
  try {
105
- currentData = migrationsToApply[i](currentData);
200
+ currentData = step.migrate(currentData);
106
201
  }
107
202
  catch (error) {
108
203
  const errorMessage = error instanceof Error ? error.message : String(error);
109
204
  return {
110
205
  ...this.getDefaultData(),
111
- warning: `Migration v${fromVer}→v${toVer} failed: ${errorMessage}`,
206
+ warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,
112
207
  };
113
208
  }
114
209
  }
@@ -120,12 +215,12 @@ class DataModel {
120
215
  *
121
216
  * All callbacks are prefixed with `__pl_` to indicate internal SDK use:
122
217
  * - `__pl_data_initial`: returns initial data for new blocks
123
- * - `__pl_data_upgrade`: upgrades versioned data from any version to latest
218
+ * - `__pl_data_migrate`: migrates versioned data from any version to latest
124
219
  * - `__pl_storage_initial`: returns initial BlockStorage as JSON string
125
220
  */
126
221
  registerCallbacks() {
127
- internal.tryRegisterCallback('__pl_data_initial', () => this._initialData());
128
- internal.tryRegisterCallback('__pl_data_upgrade', (versioned) => this.upgrade(versioned));
222
+ internal.tryRegisterCallback('__pl_data_initial', () => this.initialDataFn());
223
+ internal.tryRegisterCallback('__pl_data_migrate', (versioned) => this.migrate(versioned));
129
224
  internal.tryRegisterCallback('__pl_storage_initial', () => {
130
225
  const { version, data } = this.getDefaultData();
131
226
  const storage = block_storage.createBlockStorage(data, version);
@@ -135,4 +230,9 @@ class DataModel {
135
230
  }
136
231
 
137
232
  exports.DataModel = DataModel;
233
+ exports.DataUnrecoverableError = DataUnrecoverableError;
234
+ exports.defaultRecover = defaultRecover;
235
+ exports.defineDataVersions = defineDataVersions;
236
+ exports.isDataUnrecoverableError = isDataUnrecoverableError;
237
+ exports.makeDataVersioned = makeDataVersioned;
138
238
  //# sourceMappingURL=block_migrations.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"block_migrations.cjs","sources":["../src/block_migrations.ts"],"sourcesContent":["import { tryRegisterCallback } from './internal';\nimport { createBlockStorage } from './block_storage';\n\nexport type MigrationFn<From, To> = (prev: Readonly<From>) => To;\n\n/** Versioned data wrapper for persistence */\nexport type Versioned<T> = {\n version: number;\n data: T;\n};\n\n/** Result of upgrade operation, may include warning if migration failed */\nexport type UpgradeResult<T> = Versioned<T> & {\n warning?: string;\n};\n\n/** Internal builder for chaining migrations */\nclass DataModelBuilder<State> {\n private readonly migrationSteps: Array<(x: unknown) => unknown>;\n\n private constructor(steps: Array<(x: unknown) => unknown> = []) {\n this.migrationSteps = steps;\n }\n\n /** Start a migration chain from an initial type */\n static from<T = unknown>(): DataModelBuilder<T> {\n return new DataModelBuilder<T>();\n }\n\n /** Add a migration step */\n migrate<Next>(fn: MigrationFn<State, Next>): DataModelBuilder<Next> {\n return new DataModelBuilder<Next>([...this.migrationSteps, fn as any]);\n }\n\n /** Finalize with initial data, creating the DataModel */\n create<S>(\n initialData: () => S,\n ..._: [State] extends [S] ? [] : [never]\n ): DataModel<S> {\n return DataModel._fromBuilder<S>(this.migrationSteps, initialData);\n }\n}\n\n/**\n * DataModel defines the block's data structure, initial values, and migrations.\n * Used by BlockModelV3 to manage data state.\n *\n * @example\n * // Simple data model (no migrations)\n * const dataModel = DataModel.create<BlockData>(() => ({\n * numbers: [],\n * labels: [],\n * }));\n *\n * // Data model with migrations\n * const dataModel = DataModel\n * .from<V1>()\n * .migrate((data) => ({ ...data, labels: [] })) // v1 → v2\n * .migrate((data) => ({ ...data, description: '' })) // v2 → v3\n * .create<BlockData>(() => ({ numbers: [], labels: [], description: '' }));\n */\nexport class DataModel<State> {\n private readonly steps: Array<(x: unknown) => unknown>;\n private readonly _initialData: () => State;\n\n private constructor(steps: Array<(x: unknown) => unknown>, initialData: () => State) {\n this.steps = steps;\n this._initialData = initialData;\n }\n\n /** Start a migration chain from an initial type */\n static from<S>(): DataModelBuilder<S> {\n return DataModelBuilder.from<S>();\n }\n\n /** Create a data model with just initial data (no migrations) */\n static create<S>(initialData: () => S): DataModel<S> {\n return new DataModel<S>([], initialData);\n }\n\n /** Create from builder (internal use) */\n static _fromBuilder<S>(\n steps: Array<(x: unknown) => unknown>,\n initialData: () => S,\n ): DataModel<S> {\n return new DataModel<S>(steps, initialData);\n }\n\n /**\n * Latest version number.\n * Version 1 = initial state, each migration adds 1.\n */\n get version(): number {\n return this.steps.length + 1;\n }\n\n /** Number of migration steps */\n get migrationCount(): number {\n return this.steps.length;\n }\n\n /** Get initial data */\n initialData(): State {\n return this._initialData();\n }\n\n /** Get default data wrapped with current version */\n getDefaultData(): Versioned<State> {\n return { version: this.version, data: this._initialData() };\n }\n\n /**\n * Upgrade versioned data from any version to the latest.\n * Applies only the migrations needed (skips already-applied ones).\n * If a migration fails, returns default data with a warning.\n */\n upgrade(versioned: Versioned<unknown>): UpgradeResult<State> {\n const { version: fromVersion, data } = versioned;\n\n if (fromVersion > this.version) {\n throw new Error(\n `Cannot downgrade from version ${fromVersion} to ${this.version}`,\n );\n }\n\n if (fromVersion === this.version) {\n return { version: this.version, data: data as State };\n }\n\n // Apply migrations starting from (fromVersion - 1) index\n // Version 1 -> no migrations applied yet -> start at index 0\n // Version 2 -> migration[0] already applied -> start at index 1\n const startIndex = fromVersion - 1;\n const migrationsToApply = this.steps.slice(startIndex);\n\n let currentData: unknown = data;\n for (let i = 0; i < migrationsToApply.length; i++) {\n const stepIndex = startIndex + i;\n const fromVer = stepIndex + 1;\n const toVer = stepIndex + 2;\n try {\n currentData = migrationsToApply[i](currentData);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n ...this.getDefaultData(),\n warning: `Migration v${fromVer}→v${toVer} failed: ${errorMessage}`,\n };\n }\n }\n\n return { version: this.version, data: currentData as State };\n }\n\n /**\n * Register callbacks for use in the VM.\n * Called by BlockModelV3.create() to set up internal callbacks.\n *\n * All callbacks are prefixed with `__pl_` to indicate internal SDK use:\n * - `__pl_data_initial`: returns initial data for new blocks\n * - `__pl_data_upgrade`: upgrades versioned data from any version to latest\n * - `__pl_storage_initial`: returns initial BlockStorage as JSON string\n */\n registerCallbacks(): void {\n tryRegisterCallback('__pl_data_initial', () => this._initialData());\n tryRegisterCallback('__pl_data_upgrade', (versioned: Versioned<unknown>) => this.upgrade(versioned));\n tryRegisterCallback('__pl_storage_initial', () => {\n const { version, data } = this.getDefaultData();\n const storage = createBlockStorage(data, version);\n return JSON.stringify(storage);\n });\n }\n}\n"],"names":["tryRegisterCallback","createBlockStorage"],"mappings":"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"}
1
+ {"version":3,"file":"block_migrations.cjs","sources":["../src/block_migrations.ts"],"sourcesContent":["import { tryRegisterCallback } from './internal';\nimport { createBlockStorage, DATA_MODEL_DEFAULT_VERSION } from './block_storage';\n\nexport type DataVersionKey = string;\nexport type DataVersionMap = Record<string, unknown>;\nexport type DataMigrateFn<From, To> = (prev: Readonly<From>) => To;\nexport type DataCreateFn<T> = () => T;\nexport type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;\n\n/**\n * Helper to define version keys with literal type inference and runtime validation.\n * - Validates that all version values are unique\n * - Eliminates need for `as const` assertion\n *\n * @throws Error if duplicate version values are found\n *\n * @example\n * const Version = defineDataVersions({\n * Initial: 'v1',\n * AddedLabels: 'v2',\n * });\n *\n * type VersionedData = {\n * [Version.Initial]: DataV1;\n * [Version.AddedLabels]: DataV2;\n * };\n */\nexport function defineDataVersions<const T extends Record<string, string>>(versions: T): T {\n const values = Object.values(versions);\n const emptyKeys = Object.keys(versions).filter((key) => versions[key] === '');\n if (emptyKeys.length > 0) {\n throw new Error(`Version values must be non-empty strings (empty: ${emptyKeys.join(', ')})`);\n }\n const unique = new Set(values);\n if (unique.size !== values.length) {\n const duplicates = values.filter((v, i) => values.indexOf(v) !== i);\n throw new Error(`Duplicate version values: ${[...new Set(duplicates)].join(', ')}`);\n }\n return versions;\n}\n\n/** Versioned data wrapper for persistence */\nexport type DataVersioned<T> = {\n version: DataVersionKey;\n data: T;\n};\n\n/** Create a DataVersioned wrapper with correct shape */\nexport function makeDataVersioned<T>(version: DataVersionKey, data: T): DataVersioned<T> {\n return { version, data };\n}\n\n/** Result of migration operation, may include warning if migration failed */\nexport type DataMigrationResult<T> = DataVersioned<T> & {\n warning?: string;\n};\n\n/** Thrown by recover() to signal unrecoverable data. */\nexport class DataUnrecoverableError extends Error {\n name = 'DataUnrecoverableError';\n constructor(dataVersion: DataVersionKey) {\n super(`Unknown version '${dataVersion}'`);\n }\n}\n\nexport function isDataUnrecoverableError(error: unknown): error is DataUnrecoverableError {\n return error instanceof Error && error.name === 'DataUnrecoverableError';\n}\n\ntype MigrationStep = {\n fromVersion: DataVersionKey;\n toVersion: DataVersionKey;\n migrate: (data: unknown) => unknown;\n};\n\n/** Default recover function for unknown versions */\nexport const defaultRecover: DataRecoverFn<never> = (version, _data) => {\n throw new DataUnrecoverableError(version);\n};\n\n/** Internal builder for chaining migrations */\nclass DataModelBuilder<\n VersionedData extends DataVersionMap,\n CurrentVersion extends keyof VersionedData & string,\n> {\n private readonly versionChain: DataVersionKey[];\n private readonly migrationSteps: MigrationStep[];\n private readonly recoverFn?: DataRecoverFn<VersionedData[CurrentVersion]>;\n\n private constructor(\n versionChain: DataVersionKey[],\n steps: MigrationStep[] = [],\n recoverFn?: DataRecoverFn<VersionedData[CurrentVersion]>,\n ) {\n this.versionChain = versionChain;\n this.migrationSteps = steps;\n this.recoverFn = recoverFn;\n }\n\n /** Start a migration chain from an initial version */\n static from<\n VersionedData extends DataVersionMap,\n InitialVersion extends keyof VersionedData & string = keyof VersionedData & string,\n >(initialVersion: InitialVersion): DataModelBuilder<VersionedData, InitialVersion> {\n return new DataModelBuilder<VersionedData, InitialVersion>([initialVersion]);\n }\n\n /** Add a migration step to the target version */\n migrate<NextVersion extends keyof VersionedData & string>(\n nextVersion: NextVersion,\n fn: DataMigrateFn<VersionedData[CurrentVersion], VersionedData[NextVersion]>,\n ): DataModelBuilder<VersionedData, NextVersion> {\n if (this.versionChain.includes(nextVersion)) {\n throw new Error(`Duplicate version '${nextVersion}' in migration chain`);\n }\n const fromVersion = this.versionChain[this.versionChain.length - 1];\n const step: MigrationStep = { fromVersion, toVersion: nextVersion, migrate: fn as (data: unknown) => unknown };\n return new DataModelBuilder<VersionedData, NextVersion>(\n [...this.versionChain, nextVersion],\n [...this.migrationSteps, step],\n );\n }\n\n /** Set recovery handler for unknown or unsupported versions */\n recover(\n fn: DataRecoverFn<VersionedData[CurrentVersion]>,\n ): DataModelBuilder<VersionedData, CurrentVersion> {\n return new DataModelBuilder<VersionedData, CurrentVersion>(\n [...this.versionChain],\n [...this.migrationSteps],\n fn,\n );\n }\n\n /** Finalize with initial data, creating the DataModel */\n create<S extends VersionedData[CurrentVersion]>(\n initialData: DataCreateFn<S>,\n ..._: [VersionedData[CurrentVersion]] extends [S] ? [] : [never]\n ): DataModel<S> {\n return DataModel._fromBuilder<S>(\n this.versionChain,\n this.migrationSteps,\n initialData,\n this.recoverFn as DataRecoverFn<S> | undefined,\n );\n }\n}\n\n/**\n * DataModel defines the block's data structure, initial values, and migrations.\n * Used by BlockModelV3 to manage data state.\n *\n * @example\n * // Simple data model (no migrations)\n * const dataModel = DataModel.create<BlockData>(() => ({\n * numbers: [],\n * labels: [],\n * }));\n *\n * // Data model with migrations\n * const Version = defineDataVersions({\n * V1: 'v1',\n * V2: 'v2',\n * V3: 'v3',\n * });\n *\n * type VersionedData = {\n * [Version.V1]: { numbers: number[] };\n * [Version.V2]: { numbers: number[]; labels: string[] };\n * [Version.V3]: { numbers: number[]; labels: string[]; description: string };\n * };\n *\n * const dataModel = DataModel\n * .from<VersionedData>(Version.V1)\n * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))\n * .migrate(Version.V3, (data) => ({ ...data, description: '' }))\n * .recover((version, data) => {\n * if (version === 'legacy' && typeof data === 'object' && data !== null && 'numbers' in data) {\n * return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };\n * }\n * return defaultRecover(version, data);\n * })\n * .create(() => ({ numbers: [], labels: [], description: '' }));\n */\nexport class DataModel<State> {\n private readonly versionChain: DataVersionKey[];\n private readonly steps: MigrationStep[];\n private readonly initialDataFn: () => State;\n private readonly recoverFn: DataRecoverFn<State>;\n\n private constructor(\n versionChain: DataVersionKey[],\n steps: MigrationStep[],\n initialData: () => State,\n recover: DataRecoverFn<State> = defaultRecover as DataRecoverFn<State>,\n ) {\n if (versionChain.length === 0) {\n throw new Error('DataModel requires at least one version key');\n }\n this.versionChain = versionChain;\n this.steps = steps;\n this.initialDataFn = initialData;\n this.recoverFn = recover;\n }\n\n /** Start a migration chain from an initial type */\n static from<\n VersionedData extends DataVersionMap,\n InitialVersion extends keyof VersionedData & string = keyof VersionedData & string,\n >(initialVersion: InitialVersion): DataModelBuilder<VersionedData, InitialVersion> {\n return DataModelBuilder.from<VersionedData, InitialVersion>(initialVersion);\n }\n\n /** Create a data model with just initial data (no migrations) */\n static create<S>(initialData: () => S, version: DataVersionKey = DATA_MODEL_DEFAULT_VERSION): DataModel<S> {\n return new DataModel<S>([version], [], initialData);\n }\n\n /** Create from builder (internal use) */\n static _fromBuilder<S>(\n versionChain: DataVersionKey[],\n steps: MigrationStep[],\n initialData: () => S,\n recover?: DataRecoverFn<S>,\n ): DataModel<S> {\n return new DataModel<S>(versionChain, steps, initialData, recover);\n }\n\n /**\n * Latest version key.\n */\n get version(): DataVersionKey {\n return this.versionChain[this.versionChain.length - 1];\n }\n\n /** Number of migration steps */\n get migrationCount(): number {\n return this.steps.length;\n }\n\n /** Get initial data */\n initialData(): State {\n return this.initialDataFn();\n }\n\n /** Get default data wrapped with current version */\n getDefaultData(): DataVersioned<State> {\n return makeDataVersioned(this.version, this.initialDataFn());\n }\n\n private recoverFrom(data: unknown, version: DataVersionKey): DataMigrationResult<State> {\n try {\n return { version: this.version, data: this.recoverFn(version, data) };\n } catch (error) {\n if (isDataUnrecoverableError(error)) {\n return { ...this.getDefaultData(), warning: error.message };\n }\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n ...this.getDefaultData(),\n warning: `Recover failed for version '${version}': ${errorMessage}`,\n };\n }\n }\n\n /**\n * Migrate versioned data from any version to the latest.\n * Applies only the migrations needed (skips already-applied ones).\n * If a migration fails, returns default data with a warning.\n */\n migrate(versioned: DataVersioned<unknown>): DataMigrationResult<State> {\n const { version: fromVersion, data } = versioned;\n\n if (fromVersion === this.version) {\n return { version: this.version, data: data as State };\n }\n\n const startIndex = this.versionChain.indexOf(fromVersion);\n if (startIndex < 0) {\n return this.recoverFrom(data, fromVersion);\n }\n\n let currentData: unknown = data;\n for (let i = startIndex; i < this.steps.length; i++) {\n const step = this.steps[i];\n try {\n currentData = step.migrate(currentData);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n ...this.getDefaultData(),\n warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,\n };\n }\n }\n\n return { version: this.version, data: currentData as State };\n }\n\n /**\n * Register callbacks for use in the VM.\n * Called by BlockModelV3.create() to set up internal callbacks.\n *\n * All callbacks are prefixed with `__pl_` to indicate internal SDK use:\n * - `__pl_data_initial`: returns initial data for new blocks\n * - `__pl_data_migrate`: migrates versioned data from any version to latest\n * - `__pl_storage_initial`: returns initial BlockStorage as JSON string\n */\n registerCallbacks(): void {\n tryRegisterCallback('__pl_data_initial', () => this.initialDataFn());\n tryRegisterCallback('__pl_data_migrate', (versioned: DataVersioned<unknown>) => this.migrate(versioned));\n tryRegisterCallback('__pl_storage_initial', () => {\n const { version, data } = this.getDefaultData();\n const storage = createBlockStorage(data, version);\n return JSON.stringify(storage);\n });\n }\n}\n"],"names":["DATA_MODEL_DEFAULT_VERSION","tryRegisterCallback","createBlockStorage"],"mappings":"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"}
@@ -1,23 +1,65 @@
1
- export type MigrationFn<From, To> = (prev: Readonly<From>) => To;
1
+ export type DataVersionKey = string;
2
+ export type DataVersionMap = Record<string, unknown>;
3
+ export type DataMigrateFn<From, To> = (prev: Readonly<From>) => To;
4
+ export type DataCreateFn<T> = () => T;
5
+ export type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;
6
+ /**
7
+ * Helper to define version keys with literal type inference and runtime validation.
8
+ * - Validates that all version values are unique
9
+ * - Eliminates need for `as const` assertion
10
+ *
11
+ * @throws Error if duplicate version values are found
12
+ *
13
+ * @example
14
+ * const Version = defineDataVersions({
15
+ * Initial: 'v1',
16
+ * AddedLabels: 'v2',
17
+ * });
18
+ *
19
+ * type VersionedData = {
20
+ * [Version.Initial]: DataV1;
21
+ * [Version.AddedLabels]: DataV2;
22
+ * };
23
+ */
24
+ export declare function defineDataVersions<const T extends Record<string, string>>(versions: T): T;
2
25
  /** Versioned data wrapper for persistence */
3
- export type Versioned<T> = {
4
- version: number;
26
+ export type DataVersioned<T> = {
27
+ version: DataVersionKey;
5
28
  data: T;
6
29
  };
7
- /** Result of upgrade operation, may include warning if migration failed */
8
- export type UpgradeResult<T> = Versioned<T> & {
30
+ /** Create a DataVersioned wrapper with correct shape */
31
+ export declare function makeDataVersioned<T>(version: DataVersionKey, data: T): DataVersioned<T>;
32
+ /** Result of migration operation, may include warning if migration failed */
33
+ export type DataMigrationResult<T> = DataVersioned<T> & {
9
34
  warning?: string;
10
35
  };
36
+ /** Thrown by recover() to signal unrecoverable data. */
37
+ export declare class DataUnrecoverableError extends Error {
38
+ name: string;
39
+ constructor(dataVersion: DataVersionKey);
40
+ }
41
+ export declare function isDataUnrecoverableError(error: unknown): error is DataUnrecoverableError;
42
+ type MigrationStep = {
43
+ fromVersion: DataVersionKey;
44
+ toVersion: DataVersionKey;
45
+ migrate: (data: unknown) => unknown;
46
+ };
47
+ /** Default recover function for unknown versions */
48
+ export declare const defaultRecover: DataRecoverFn<never>;
11
49
  /** Internal builder for chaining migrations */
12
- declare class DataModelBuilder<State> {
50
+ declare class DataModelBuilder<VersionedData extends DataVersionMap, CurrentVersion extends keyof VersionedData & string> {
51
+ private readonly versionChain;
13
52
  private readonly migrationSteps;
53
+ private readonly recoverFn?;
14
54
  private constructor();
15
- /** Start a migration chain from an initial type */
16
- static from<T = unknown>(): DataModelBuilder<T>;
17
- /** Add a migration step */
18
- migrate<Next>(fn: MigrationFn<State, Next>): DataModelBuilder<Next>;
55
+ /** Start a migration chain from an initial version */
56
+ static from<VersionedData extends DataVersionMap, InitialVersion extends keyof VersionedData & string = keyof VersionedData & string>(initialVersion: InitialVersion): DataModelBuilder<VersionedData, InitialVersion>;
57
+ /** Add a migration step to the target version */
58
+ migrate<NextVersion extends keyof VersionedData & string>(nextVersion: NextVersion, fn: DataMigrateFn<VersionedData[CurrentVersion], VersionedData[NextVersion]>): DataModelBuilder<VersionedData, NextVersion>;
59
+ /** Set recovery handler for unknown or unsupported versions */
60
+ recover(fn: DataRecoverFn<VersionedData[CurrentVersion]>): DataModelBuilder<VersionedData, CurrentVersion>;
19
61
  /** Finalize with initial data, creating the DataModel */
20
- create<S>(initialData: () => S, ..._: [State] extends [S] ? [] : [never]): DataModel<S>;
62
+ create<S extends VersionedData[CurrentVersion]>(initialData: DataCreateFn<S>, ..._: [VersionedData[CurrentVersion]] extends [S] ? [] : [never]): DataModel<S>;
21
63
  }
22
64
  /**
23
65
  * DataModel defines the block's data structure, initial values, and migrations.
@@ -31,46 +73,66 @@ declare class DataModelBuilder<State> {
31
73
  * }));
32
74
  *
33
75
  * // Data model with migrations
76
+ * const Version = defineDataVersions({
77
+ * V1: 'v1',
78
+ * V2: 'v2',
79
+ * V3: 'v3',
80
+ * });
81
+ *
82
+ * type VersionedData = {
83
+ * [Version.V1]: { numbers: number[] };
84
+ * [Version.V2]: { numbers: number[]; labels: string[] };
85
+ * [Version.V3]: { numbers: number[]; labels: string[]; description: string };
86
+ * };
87
+ *
34
88
  * const dataModel = DataModel
35
- * .from<V1>()
36
- * .migrate((data) => ({ ...data, labels: [] })) // v1 → v2
37
- * .migrate((data) => ({ ...data, description: '' })) // v2 → v3
38
- * .create<BlockData>(() => ({ numbers: [], labels: [], description: '' }));
89
+ * .from<VersionedData>(Version.V1)
90
+ * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
91
+ * .migrate(Version.V3, (data) => ({ ...data, description: '' }))
92
+ * .recover((version, data) => {
93
+ * if (version === 'legacy' && typeof data === 'object' && data !== null && 'numbers' in data) {
94
+ * return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };
95
+ * }
96
+ * return defaultRecover(version, data);
97
+ * })
98
+ * .create(() => ({ numbers: [], labels: [], description: '' }));
39
99
  */
40
100
  export declare class DataModel<State> {
101
+ private readonly versionChain;
41
102
  private readonly steps;
42
- private readonly _initialData;
103
+ private readonly initialDataFn;
104
+ private readonly recoverFn;
43
105
  private constructor();
44
106
  /** Start a migration chain from an initial type */
45
- static from<S>(): DataModelBuilder<S>;
107
+ static from<VersionedData extends DataVersionMap, InitialVersion extends keyof VersionedData & string = keyof VersionedData & string>(initialVersion: InitialVersion): DataModelBuilder<VersionedData, InitialVersion>;
46
108
  /** Create a data model with just initial data (no migrations) */
47
- static create<S>(initialData: () => S): DataModel<S>;
109
+ static create<S>(initialData: () => S, version?: DataVersionKey): DataModel<S>;
48
110
  /** Create from builder (internal use) */
49
- static _fromBuilder<S>(steps: Array<(x: unknown) => unknown>, initialData: () => S): DataModel<S>;
111
+ static _fromBuilder<S>(versionChain: DataVersionKey[], steps: MigrationStep[], initialData: () => S, recover?: DataRecoverFn<S>): DataModel<S>;
50
112
  /**
51
- * Latest version number.
52
- * Version 1 = initial state, each migration adds 1.
113
+ * Latest version key.
53
114
  */
54
- get version(): number;
115
+ get version(): DataVersionKey;
55
116
  /** Number of migration steps */
56
117
  get migrationCount(): number;
57
118
  /** Get initial data */
58
119
  initialData(): State;
59
120
  /** Get default data wrapped with current version */
60
- getDefaultData(): Versioned<State>;
121
+ getDefaultData(): DataVersioned<State>;
122
+ private recoverFrom;
61
123
  /**
62
- * Upgrade versioned data from any version to the latest.
124
+ * Migrate versioned data from any version to the latest.
63
125
  * Applies only the migrations needed (skips already-applied ones).
64
126
  * If a migration fails, returns default data with a warning.
65
127
  */
66
- upgrade(versioned: Versioned<unknown>): UpgradeResult<State>;
128
+ migrate(versioned: DataVersioned<unknown>): DataMigrationResult<State>;
67
129
  /**
68
130
  * Register callbacks for use in the VM.
69
131
  * Called by BlockModelV3.create() to set up internal callbacks.
70
132
  *
71
133
  * All callbacks are prefixed with `__pl_` to indicate internal SDK use:
72
134
  * - `__pl_data_initial`: returns initial data for new blocks
73
- * - `__pl_data_upgrade`: upgrades versioned data from any version to latest
135
+ * - `__pl_data_migrate`: migrates versioned data from any version to latest
74
136
  * - `__pl_storage_initial`: returns initial BlockStorage as JSON string
75
137
  */
76
138
  registerCallbacks(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"block_migrations.d.ts","sourceRoot":"","sources":["../src/block_migrations.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;AAEjE,6CAA6C;AAC7C,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,CAAC,CAAC;CACT,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,+CAA+C;AAC/C,cAAM,gBAAgB,CAAC,KAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiC;IAEhE,OAAO;IAIP,mDAAmD;IACnD,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,OAAO,KAAK,gBAAgB,CAAC,CAAC,CAAC;IAI/C,2BAA2B;IAC3B,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC;IAInE,yDAAyD;IACzD,MAAM,CAAC,CAAC,EACN,WAAW,EAAE,MAAM,CAAC,EACpB,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GACvC,SAAS,CAAC,CAAC,CAAC;CAGhB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,SAAS,CAAC,KAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAE3C,OAAO;IAKP,mDAAmD;IACnD,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC;IAIrC,iEAAiE;IACjE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAIpD,yCAAyC;IACzC,MAAM,CAAC,YAAY,CAAC,CAAC,EACnB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,EACrC,WAAW,EAAE,MAAM,CAAC,GACnB,SAAS,CAAC,CAAC,CAAC;IAIf;;;OAGG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,gCAAgC;IAChC,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,uBAAuB;IACvB,WAAW,IAAI,KAAK;IAIpB,oDAAoD;IACpD,cAAc,IAAI,SAAS,CAAC,KAAK,CAAC;IAIlC;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC;IAsC5D;;;;;;;;OAQG;IACH,iBAAiB,IAAI,IAAI;CAS1B"}
1
+ {"version":3,"file":"block_migrations.d.ts","sourceRoot":"","sources":["../src/block_migrations.ts"],"names":[],"mappings":"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"}