@platforma-sdk/model 1.51.9 → 1.52.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bconfig/lambdas.d.ts +26 -4
- package/dist/bconfig/lambdas.d.ts.map +1 -1
- package/dist/bconfig/v3.d.ts +4 -2
- package/dist/bconfig/v3.d.ts.map +1 -1
- package/dist/block_api_v3.d.ts +32 -0
- package/dist/block_api_v3.d.ts.map +1 -0
- package/dist/block_migrations.cjs +138 -0
- package/dist/block_migrations.cjs.map +1 -0
- package/dist/block_migrations.d.ts +79 -0
- package/dist/block_migrations.d.ts.map +1 -0
- package/dist/block_migrations.js +136 -0
- package/dist/block_migrations.js.map +1 -0
- package/dist/block_model.cjs +222 -0
- package/dist/block_model.cjs.map +1 -0
- package/dist/block_model.d.ts +132 -0
- package/dist/block_model.d.ts.map +1 -0
- package/dist/block_model.js +220 -0
- package/dist/block_model.js.map +1 -0
- package/dist/block_storage.cjs +244 -0
- package/dist/block_storage.cjs.map +1 -0
- package/dist/block_storage.d.ts +208 -0
- package/dist/block_storage.d.ts.map +1 -0
- package/dist/block_storage.js +225 -0
- package/dist/block_storage.js.map +1 -0
- package/dist/block_storage_vm.cjs +264 -0
- package/dist/block_storage_vm.cjs.map +1 -0
- package/dist/block_storage_vm.d.ts +67 -0
- package/dist/block_storage_vm.d.ts.map +1 -0
- package/dist/block_storage_vm.js +260 -0
- package/dist/block_storage_vm.js.map +1 -0
- package/dist/builder.cjs +9 -6
- package/dist/builder.cjs.map +1 -1
- package/dist/builder.d.ts +15 -30
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +10 -7
- package/dist/builder.js.map +1 -1
- package/dist/components/PFrameForGraphs.cjs.map +1 -1
- package/dist/components/PFrameForGraphs.d.ts +2 -2
- package/dist/components/PFrameForGraphs.d.ts.map +1 -1
- package/dist/components/PFrameForGraphs.js.map +1 -1
- package/dist/components/PlDataTable.cjs.map +1 -1
- package/dist/components/PlDataTable.d.ts +3 -3
- package/dist/components/PlDataTable.d.ts.map +1 -1
- package/dist/components/PlDataTable.js.map +1 -1
- package/dist/index.cjs +25 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +38 -0
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.ts +21 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +36 -1
- package/dist/internal.js.map +1 -1
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/pframe_utils/columns.cjs.map +1 -1
- package/dist/pframe_utils/columns.d.ts +3 -3
- package/dist/pframe_utils/columns.d.ts.map +1 -1
- package/dist/pframe_utils/columns.js.map +1 -1
- package/dist/platforma.d.ts +18 -3
- package/dist/platforma.d.ts.map +1 -1
- package/dist/render/api.cjs +43 -16
- package/dist/render/api.cjs.map +1 -1
- package/dist/render/api.d.ts +19 -7
- package/dist/render/api.d.ts.map +1 -1
- package/dist/render/api.js +42 -17
- package/dist/render/api.js.map +1 -1
- package/dist/render/internal.cjs.map +1 -1
- package/dist/render/internal.d.ts +3 -1
- package/dist/render/internal.d.ts.map +1 -1
- package/dist/render/internal.js.map +1 -1
- package/package.json +7 -7
- package/src/bconfig/lambdas.ts +35 -4
- package/src/bconfig/v3.ts +12 -2
- package/src/block_api_v3.ts +49 -0
- package/src/block_migrations.ts +173 -0
- package/src/block_model.ts +440 -0
- package/src/block_storage.test.ts +258 -0
- package/src/block_storage.ts +365 -0
- package/src/block_storage_vm.ts +349 -0
- package/src/builder.ts +24 -59
- package/src/components/PFrameForGraphs.ts +2 -2
- package/src/components/PlDataTable.ts +3 -3
- package/src/index.ts +3 -0
- package/src/internal.ts +51 -0
- package/src/pframe_utils/columns.ts +3 -3
- package/src/platforma.ts +31 -5
- package/src/render/api.ts +52 -21
- package/src/render/internal.ts +3 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
BLOCK_STORAGE_KEY,
|
|
4
|
+
BLOCK_STORAGE_SCHEMA_VERSION,
|
|
5
|
+
createBlockStorage,
|
|
6
|
+
defaultBlockStorageHandlers,
|
|
7
|
+
getFromStorage,
|
|
8
|
+
getPluginData,
|
|
9
|
+
getPluginNames,
|
|
10
|
+
getStorageData,
|
|
11
|
+
getStorageDataVersion,
|
|
12
|
+
isBlockStorage,
|
|
13
|
+
mergeBlockStorageHandlers,
|
|
14
|
+
normalizeBlockStorage,
|
|
15
|
+
removePluginData,
|
|
16
|
+
setPluginData,
|
|
17
|
+
updateStorageData,
|
|
18
|
+
updateStorageDataVersion,
|
|
19
|
+
updateStorage,
|
|
20
|
+
} from './block_storage';
|
|
21
|
+
|
|
22
|
+
describe('BlockStorage', () => {
|
|
23
|
+
describe('BLOCK_STORAGE_KEY and BLOCK_STORAGE_SCHEMA_VERSION', () => {
|
|
24
|
+
it('should have correct key constant', () => {
|
|
25
|
+
expect(typeof BLOCK_STORAGE_KEY).toBe('string');
|
|
26
|
+
expect(BLOCK_STORAGE_KEY).toBe('__pl_a7f3e2b9__');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have correct schema version', () => {
|
|
30
|
+
expect(BLOCK_STORAGE_SCHEMA_VERSION).toBe('v1');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('isBlockStorage', () => {
|
|
35
|
+
it('should return true for valid BlockStorage with discriminator', () => {
|
|
36
|
+
const storage = createBlockStorage({});
|
|
37
|
+
expect(isBlockStorage(storage)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return true for BlockStorage with plugin data', () => {
|
|
41
|
+
const storage = setPluginData(createBlockStorage({ foo: 'bar' }), 'test', { data: 123 });
|
|
42
|
+
expect(isBlockStorage(storage)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return false for null', () => {
|
|
46
|
+
expect(isBlockStorage(null)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return false for undefined', () => {
|
|
50
|
+
expect(isBlockStorage(undefined)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return false for primitive values', () => {
|
|
54
|
+
expect(isBlockStorage(42)).toBe(false);
|
|
55
|
+
expect(isBlockStorage('string')).toBe(false);
|
|
56
|
+
expect(isBlockStorage(true)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return false for objects without discriminator', () => {
|
|
60
|
+
expect(isBlockStorage({ __dataVersion: 1, __data: {} })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false for objects with wrong discriminator value', () => {
|
|
64
|
+
expect(isBlockStorage({ [BLOCK_STORAGE_KEY]: 'wrong', __dataVersion: 1, __data: {} })).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('createBlockStorage', () => {
|
|
69
|
+
it('should create storage with discriminator key and default values', () => {
|
|
70
|
+
const storage = createBlockStorage();
|
|
71
|
+
expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
72
|
+
expect(storage.__dataVersion).toBe(1);
|
|
73
|
+
expect(storage.__data).toEqual({});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should create storage with custom initial data', () => {
|
|
77
|
+
const data = { numbers: [1, 2, 3] };
|
|
78
|
+
const storage = createBlockStorage(data);
|
|
79
|
+
expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
80
|
+
expect(storage.__dataVersion).toBe(1);
|
|
81
|
+
expect(storage.__data).toEqual(data);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should create storage with custom version', () => {
|
|
85
|
+
const storage = createBlockStorage({ foo: 'bar' }, 5);
|
|
86
|
+
expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
87
|
+
expect(storage.__dataVersion).toBe(5);
|
|
88
|
+
expect(storage.__data).toEqual({ foo: 'bar' });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('normalizeBlockStorage', () => {
|
|
93
|
+
it('should return BlockStorage as-is', () => {
|
|
94
|
+
const storage = createBlockStorage({ data: 'test' }, 2);
|
|
95
|
+
const normalized = normalizeBlockStorage(storage);
|
|
96
|
+
expect(normalized).toEqual(storage);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should wrap legacy data in BlockStorage structure', () => {
|
|
100
|
+
const legacyData = { numbers: [1, 2, 3], name: 'test' };
|
|
101
|
+
const normalized = normalizeBlockStorage(legacyData);
|
|
102
|
+
expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
103
|
+
expect(normalized.__dataVersion).toBe(1);
|
|
104
|
+
expect(normalized.__data).toEqual(legacyData);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should wrap primitive legacy data', () => {
|
|
108
|
+
const normalized = normalizeBlockStorage('simple string');
|
|
109
|
+
expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
110
|
+
expect(normalized.__dataVersion).toBe(1);
|
|
111
|
+
expect(normalized.__data).toBe('simple string');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should wrap null legacy data', () => {
|
|
115
|
+
const normalized = normalizeBlockStorage(null);
|
|
116
|
+
expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
117
|
+
expect(normalized.__dataVersion).toBe(1);
|
|
118
|
+
expect(normalized.__data).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Data access functions', () => {
|
|
123
|
+
const storage = createBlockStorage({ count: 42 }, 3);
|
|
124
|
+
|
|
125
|
+
it('getStorageData should return the data', () => {
|
|
126
|
+
expect(getStorageData(storage)).toEqual({ count: 42 });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('getStorageDataVersion should return the version', () => {
|
|
130
|
+
expect(getStorageDataVersion(storage)).toBe(3);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('updateStorageData should return new storage with updated data', () => {
|
|
134
|
+
const newStorage = updateStorageData(storage, { operation: 'update-data', value: { count: 100 } });
|
|
135
|
+
expect(newStorage.__data).toEqual({ count: 100 });
|
|
136
|
+
expect(newStorage.__dataVersion).toBe(3);
|
|
137
|
+
expect(newStorage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
138
|
+
// Original should be unchanged
|
|
139
|
+
expect(storage.__data).toEqual({ count: 42 });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('updateStorageDataVersion should return new storage with updated version', () => {
|
|
143
|
+
const newStorage = updateStorageDataVersion(storage, 5);
|
|
144
|
+
expect(newStorage.__dataVersion).toBe(5);
|
|
145
|
+
expect(newStorage.__data).toEqual({ count: 42 });
|
|
146
|
+
expect(newStorage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
|
|
147
|
+
// Original should be unchanged
|
|
148
|
+
expect(storage.__dataVersion).toBe(3);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Plugin data functions', () => {
|
|
153
|
+
const baseStorage = createBlockStorage({});
|
|
154
|
+
|
|
155
|
+
it('setPluginData should add plugin data', () => {
|
|
156
|
+
const storage = setPluginData(baseStorage, 'table', { columns: ['a', 'b'] });
|
|
157
|
+
expect(storage['@plugin/table']).toEqual({ columns: ['a', 'b'] });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('getPluginData should retrieve plugin data', () => {
|
|
161
|
+
const storage = setPluginData(baseStorage, 'chart', { type: 'bar' });
|
|
162
|
+
expect(getPluginData(storage, 'chart')).toEqual({ type: 'bar' });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('getPluginData should return undefined for missing plugin', () => {
|
|
166
|
+
expect(getPluginData(baseStorage, 'nonexistent')).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('removePluginData should remove plugin data', () => {
|
|
170
|
+
let storage = setPluginData(baseStorage, 'toRemove', { data: 'test' });
|
|
171
|
+
storage = setPluginData(storage, 'toKeep', { other: 'data' });
|
|
172
|
+
const result = removePluginData(storage, 'toRemove');
|
|
173
|
+
expect(result['@plugin/toRemove']).toBeUndefined();
|
|
174
|
+
expect(result['@plugin/toKeep']).toEqual({ other: 'data' });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('getPluginNames should return all plugin names', () => {
|
|
178
|
+
let storage = createBlockStorage({});
|
|
179
|
+
storage = setPluginData(storage, 'alpha', {});
|
|
180
|
+
storage = setPluginData(storage, 'beta', {});
|
|
181
|
+
storage = setPluginData(storage, 'gamma', {});
|
|
182
|
+
const names = getPluginNames(storage);
|
|
183
|
+
expect(names.sort()).toEqual(['alpha', 'beta', 'gamma']);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('getPluginNames should return empty array when no plugins', () => {
|
|
187
|
+
expect(getPluginNames(baseStorage)).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Generic storage access', () => {
|
|
192
|
+
const storage = createBlockStorage('hello', 2);
|
|
193
|
+
|
|
194
|
+
it('getFromStorage should get __data', () => {
|
|
195
|
+
expect(getFromStorage(storage, '__data')).toBe('hello');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('getFromStorage should get __dataVersion', () => {
|
|
199
|
+
expect(getFromStorage(storage, '__dataVersion')).toBe(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('updateStorage should update any key', () => {
|
|
203
|
+
const updated = updateStorage(storage, '__data', 'world');
|
|
204
|
+
expect(updated.__data).toBe('world');
|
|
205
|
+
expect(storage.__data).toBe('hello'); // immutable
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('BlockStorageHandlers', () => {
|
|
210
|
+
describe('defaultBlockStorageHandlers', () => {
|
|
211
|
+
it('transformStateForStorage should replace data', () => {
|
|
212
|
+
const storage = createBlockStorage('old');
|
|
213
|
+
const result = defaultBlockStorageHandlers.transformStateForStorage(storage, 'new');
|
|
214
|
+
expect(result.__data).toBe('new');
|
|
215
|
+
expect(result.__dataVersion).toBe(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('deriveStateForArgs should return data directly', () => {
|
|
219
|
+
const storage = createBlockStorage({ data: 'test' });
|
|
220
|
+
expect(defaultBlockStorageHandlers.deriveStateForArgs(storage)).toEqual({ data: 'test' });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('migrateStorage should update version only', () => {
|
|
224
|
+
const storage = createBlockStorage({ data: 'test' });
|
|
225
|
+
const result = defaultBlockStorageHandlers.migrateStorage(storage, 1, 3);
|
|
226
|
+
expect(result.__dataVersion).toBe(3);
|
|
227
|
+
expect(result.__data).toEqual({ data: 'test' });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('mergeBlockStorageHandlers', () => {
|
|
232
|
+
it('should return defaults when no custom handlers provided', () => {
|
|
233
|
+
const handlers = mergeBlockStorageHandlers();
|
|
234
|
+
expect(handlers.transformStateForStorage).toBe(
|
|
235
|
+
defaultBlockStorageHandlers.transformStateForStorage,
|
|
236
|
+
);
|
|
237
|
+
expect(handlers.deriveStateForArgs).toBe(defaultBlockStorageHandlers.deriveStateForArgs);
|
|
238
|
+
expect(handlers.migrateStorage).toBe(defaultBlockStorageHandlers.migrateStorage);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should override with custom handlers', () => {
|
|
242
|
+
const customTransform = <T>(storage: ReturnType<typeof createBlockStorage<T>>, data: T) => ({
|
|
243
|
+
...storage,
|
|
244
|
+
__data: data,
|
|
245
|
+
__dataVersion: storage.__dataVersion + 1,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const handlers = mergeBlockStorageHandlers({
|
|
249
|
+
transformStateForStorage: customTransform,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(handlers.transformStateForStorage).toBe(customTransform);
|
|
253
|
+
expect(handlers.deriveStateForArgs).toBe(defaultBlockStorageHandlers.deriveStateForArgs);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockStorage - Typed storage abstraction for block persistent data.
|
|
3
|
+
*
|
|
4
|
+
* This module provides:
|
|
5
|
+
* - A typed structure for block storage with versioning and plugin support
|
|
6
|
+
* - Utility functions for manipulating storage
|
|
7
|
+
* - Handler interfaces for model-level customization
|
|
8
|
+
*
|
|
9
|
+
* @module block_storage
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Core Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Discriminator key for BlockStorage format detection.
|
|
18
|
+
* This unique hash-based key identifies data as BlockStorage vs legacy formats.
|
|
19
|
+
*/
|
|
20
|
+
export const BLOCK_STORAGE_KEY = '__pl_a7f3e2b9__';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Current BlockStorage schema version.
|
|
24
|
+
* Increment this when the storage structure itself changes (not block state migrations).
|
|
25
|
+
*/
|
|
26
|
+
export const BLOCK_STORAGE_SCHEMA_VERSION = 'v1';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Type for valid schema versions
|
|
30
|
+
*/
|
|
31
|
+
export type BlockStorageSchemaVersion = 'v1'; // Add 'v2', 'v3', etc. as schema evolves
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Plugin key type - keys starting with `@plugin/` are reserved for plugin data
|
|
35
|
+
*/
|
|
36
|
+
export type PluginKey = `@plugin/${string}`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Core BlockStorage type that holds:
|
|
40
|
+
* - __pl_a7f3e2b9__: Schema version (discriminator key identifies BlockStorage format)
|
|
41
|
+
* - __dataVersion: Version number for block data migrations
|
|
42
|
+
* - __data: The block's user-facing data (state)
|
|
43
|
+
* - @plugin/*: Optional plugin-specific data
|
|
44
|
+
*/
|
|
45
|
+
export type BlockStorage<TState = unknown> = {
|
|
46
|
+
/** Schema version - the key itself is the discriminator */
|
|
47
|
+
readonly [BLOCK_STORAGE_KEY]: BlockStorageSchemaVersion;
|
|
48
|
+
/** Version of the block data, used for migrations */
|
|
49
|
+
__dataVersion: number;
|
|
50
|
+
/** The block's user-facing data (state) */
|
|
51
|
+
__data: TState;
|
|
52
|
+
} & {
|
|
53
|
+
/** Plugin-specific data, keyed by `@plugin/<pluginName>` */
|
|
54
|
+
[K in PluginKey]?: unknown;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Type guard to check if a value is a valid BlockStorage object.
|
|
59
|
+
* Checks for the discriminator key and valid schema version.
|
|
60
|
+
*/
|
|
61
|
+
export function isBlockStorage(value: unknown): value is BlockStorage {
|
|
62
|
+
if (value === null || typeof value !== 'object') return false;
|
|
63
|
+
const obj = value as Record<string, unknown>;
|
|
64
|
+
const schemaVersion = obj[BLOCK_STORAGE_KEY];
|
|
65
|
+
// Currently only 'v1' is valid, but this allows future versions
|
|
66
|
+
return schemaVersion === 'v1'; // Add more versions as schema evolves
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Factory Functions
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a BlockStorage with the given initial data
|
|
75
|
+
*
|
|
76
|
+
* @param initialData - The initial data value (defaults to empty object)
|
|
77
|
+
* @param version - The initial data version (defaults to 1)
|
|
78
|
+
* @returns A new BlockStorage instance with discriminator key
|
|
79
|
+
*/
|
|
80
|
+
export function createBlockStorage<TState = unknown>(
|
|
81
|
+
initialData: TState = {} as TState,
|
|
82
|
+
version: number = 1,
|
|
83
|
+
): BlockStorage<TState> {
|
|
84
|
+
return {
|
|
85
|
+
[BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
|
|
86
|
+
__dataVersion: version,
|
|
87
|
+
__data: initialData,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes raw storage data to BlockStorage format.
|
|
93
|
+
* If the input is already a BlockStorage, returns it as-is.
|
|
94
|
+
* If the input is legacy format (raw state), wraps it in BlockStorage structure.
|
|
95
|
+
*
|
|
96
|
+
* @param raw - Raw storage data (may be legacy format or BlockStorage)
|
|
97
|
+
* @returns Normalized BlockStorage
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeBlockStorage<TState = unknown>(raw: unknown): BlockStorage<TState> {
|
|
100
|
+
if (isBlockStorage(raw)) {
|
|
101
|
+
return raw as BlockStorage<TState>;
|
|
102
|
+
}
|
|
103
|
+
// Legacy format: raw is the state directly
|
|
104
|
+
return createBlockStorage(raw as TState, 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Data Access & Update Functions
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets the data from BlockStorage
|
|
113
|
+
*
|
|
114
|
+
* @param storage - The BlockStorage instance
|
|
115
|
+
* @returns The data value
|
|
116
|
+
*/
|
|
117
|
+
export function getStorageData<TState>(storage: BlockStorage<TState>): TState {
|
|
118
|
+
return storage.__data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Derives data from raw block storage.
|
|
123
|
+
* This function is meant to be called from sdk/ui-vue to extract
|
|
124
|
+
* user-facing data from the raw storage returned by the middle layer.
|
|
125
|
+
*
|
|
126
|
+
* The middle layer returns raw storage (opaque to it), and the UI
|
|
127
|
+
* uses this function to derive the actual data value.
|
|
128
|
+
*
|
|
129
|
+
* @param rawStorage - Raw storage data from middle layer (may be any format)
|
|
130
|
+
* @returns The extracted data value, or undefined if storage is undefined/null
|
|
131
|
+
*/
|
|
132
|
+
export function deriveDataFromStorage<TData = unknown>(
|
|
133
|
+
rawStorage: unknown,
|
|
134
|
+
): TData {
|
|
135
|
+
// Normalize to BlockStorage format (handles legacy formats too)
|
|
136
|
+
const storage = normalizeBlockStorage<TData>(rawStorage);
|
|
137
|
+
return getStorageData(storage);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Payload for storage mutation operations. SDK defines specific operations. */
|
|
141
|
+
export type MutateStoragePayload<T = unknown> = { operation: 'update-data'; value: T };
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Updates the data in BlockStorage (immutable)
|
|
145
|
+
*
|
|
146
|
+
* @param storage - The current BlockStorage
|
|
147
|
+
* @param payload - The update payload with operation and value
|
|
148
|
+
* @returns A new BlockStorage with updated data
|
|
149
|
+
*/
|
|
150
|
+
export function updateStorageData<TValue = unknown>(
|
|
151
|
+
storage: BlockStorage<TValue>,
|
|
152
|
+
payload: MutateStoragePayload<TValue>,
|
|
153
|
+
): BlockStorage<TValue> {
|
|
154
|
+
switch (payload.operation) {
|
|
155
|
+
case 'update-data':
|
|
156
|
+
return { ...storage, __data: payload.value };
|
|
157
|
+
default:
|
|
158
|
+
throw new Error(`Unknown storage operation: ${(payload as { operation: string }).operation}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gets the data version from BlockStorage
|
|
164
|
+
*
|
|
165
|
+
* @param storage - The BlockStorage instance
|
|
166
|
+
* @returns The data version number
|
|
167
|
+
*/
|
|
168
|
+
export function getStorageDataVersion(storage: BlockStorage): number {
|
|
169
|
+
return storage.__dataVersion;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Updates the data version in BlockStorage (immutable)
|
|
174
|
+
*
|
|
175
|
+
* @param storage - The current BlockStorage
|
|
176
|
+
* @param version - The new version number
|
|
177
|
+
* @returns A new BlockStorage with updated version
|
|
178
|
+
*/
|
|
179
|
+
export function updateStorageDataVersion<TState>(
|
|
180
|
+
storage: BlockStorage<TState>,
|
|
181
|
+
version: number,
|
|
182
|
+
): BlockStorage<TState> {
|
|
183
|
+
return { ...storage, __dataVersion: version };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// Plugin Data Functions
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Gets plugin-specific data from BlockStorage
|
|
192
|
+
*
|
|
193
|
+
* @param storage - The BlockStorage instance
|
|
194
|
+
* @param pluginName - The plugin name (without `@plugin/` prefix)
|
|
195
|
+
* @returns The plugin data or undefined if not set
|
|
196
|
+
*/
|
|
197
|
+
export function getPluginData<TData = unknown>(
|
|
198
|
+
storage: BlockStorage,
|
|
199
|
+
pluginName: string,
|
|
200
|
+
): TData | undefined {
|
|
201
|
+
const key: PluginKey = `@plugin/${pluginName}`;
|
|
202
|
+
return storage[key] as TData | undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Sets plugin-specific data in BlockStorage (immutable)
|
|
207
|
+
*
|
|
208
|
+
* @param storage - The current BlockStorage
|
|
209
|
+
* @param pluginName - The plugin name (without `@plugin/` prefix)
|
|
210
|
+
* @param data - The plugin data to store
|
|
211
|
+
* @returns A new BlockStorage with updated plugin data
|
|
212
|
+
*/
|
|
213
|
+
export function setPluginData<TState>(
|
|
214
|
+
storage: BlockStorage<TState>,
|
|
215
|
+
pluginName: string,
|
|
216
|
+
data: unknown,
|
|
217
|
+
): BlockStorage<TState> {
|
|
218
|
+
const key: PluginKey = `@plugin/${pluginName}`;
|
|
219
|
+
return { ...storage, [key]: data };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Removes plugin-specific data from BlockStorage (immutable)
|
|
224
|
+
*
|
|
225
|
+
* @param storage - The current BlockStorage
|
|
226
|
+
* @param pluginName - The plugin name (without `@plugin/` prefix)
|
|
227
|
+
* @returns A new BlockStorage with the plugin data removed
|
|
228
|
+
*/
|
|
229
|
+
export function removePluginData<TState>(
|
|
230
|
+
storage: BlockStorage<TState>,
|
|
231
|
+
pluginName: string,
|
|
232
|
+
): BlockStorage<TState> {
|
|
233
|
+
const key: PluginKey = `@plugin/${pluginName}`;
|
|
234
|
+
const { [key]: _, ...rest } = storage;
|
|
235
|
+
return rest as BlockStorage<TState>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Gets all plugin names that have data stored
|
|
240
|
+
*
|
|
241
|
+
* @param storage - The BlockStorage instance
|
|
242
|
+
* @returns Array of plugin names (without `@plugin/` prefix)
|
|
243
|
+
*/
|
|
244
|
+
export function getPluginNames(storage: BlockStorage): string[] {
|
|
245
|
+
return Object.keys(storage)
|
|
246
|
+
.filter((key): key is PluginKey => key.startsWith('@plugin/'))
|
|
247
|
+
.map((key) => key.slice('@plugin/'.length));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// =============================================================================
|
|
251
|
+
// Generic Storage Access
|
|
252
|
+
// =============================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Gets a value from BlockStorage by key
|
|
256
|
+
*
|
|
257
|
+
* @param storage - The BlockStorage instance
|
|
258
|
+
* @param key - The key to retrieve
|
|
259
|
+
* @returns The value at the given key
|
|
260
|
+
*/
|
|
261
|
+
export function getFromStorage<
|
|
262
|
+
TState,
|
|
263
|
+
K extends keyof BlockStorage<TState>,
|
|
264
|
+
>(storage: BlockStorage<TState>, key: K): BlockStorage<TState>[K] {
|
|
265
|
+
return storage[key];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Updates a value in BlockStorage by key (immutable)
|
|
270
|
+
*
|
|
271
|
+
* @param storage - The current BlockStorage
|
|
272
|
+
* @param key - The key to update
|
|
273
|
+
* @param value - The new value
|
|
274
|
+
* @returns A new BlockStorage with the updated value
|
|
275
|
+
*/
|
|
276
|
+
export function updateStorage<
|
|
277
|
+
TState,
|
|
278
|
+
K extends keyof BlockStorage<TState>,
|
|
279
|
+
>(
|
|
280
|
+
storage: BlockStorage<TState>,
|
|
281
|
+
key: K,
|
|
282
|
+
value: BlockStorage<TState>[K],
|
|
283
|
+
): BlockStorage<TState> {
|
|
284
|
+
return { ...storage, [key]: value };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// =============================================================================
|
|
288
|
+
// Storage Handlers (for Phase 2 - Model-Level Customization)
|
|
289
|
+
// =============================================================================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Interface for model-configurable storage operations.
|
|
293
|
+
* These handlers allow block models to customize how storage is managed.
|
|
294
|
+
*/
|
|
295
|
+
export interface BlockStorageHandlers<TState = unknown> {
|
|
296
|
+
/**
|
|
297
|
+
* Called when setState is invoked - transforms the new state before storing.
|
|
298
|
+
* Default behavior: replaces the state directly.
|
|
299
|
+
*
|
|
300
|
+
* @param currentStorage - The current BlockStorage
|
|
301
|
+
* @param newState - The new state being set
|
|
302
|
+
* @returns The updated BlockStorage
|
|
303
|
+
*/
|
|
304
|
+
transformStateForStorage?: (
|
|
305
|
+
currentStorage: BlockStorage<TState>,
|
|
306
|
+
newState: TState,
|
|
307
|
+
) => BlockStorage<TState>;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Called when reading state for args derivation.
|
|
311
|
+
* Default behavior: returns the state directly.
|
|
312
|
+
*
|
|
313
|
+
* @param storage - The current BlockStorage
|
|
314
|
+
* @returns The state to use for args derivation
|
|
315
|
+
*/
|
|
316
|
+
deriveStateForArgs?: (storage: BlockStorage<TState>) => TState;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Called during storage schema migration.
|
|
320
|
+
* Default behavior: updates stateVersion only.
|
|
321
|
+
*
|
|
322
|
+
* @param oldStorage - The storage before migration
|
|
323
|
+
* @param fromVersion - The version migrating from
|
|
324
|
+
* @param toVersion - The version migrating to
|
|
325
|
+
* @returns The migrated BlockStorage
|
|
326
|
+
*/
|
|
327
|
+
migrateStorage?: (
|
|
328
|
+
oldStorage: BlockStorage<TState>,
|
|
329
|
+
fromVersion: number,
|
|
330
|
+
toVersion: number,
|
|
331
|
+
) => BlockStorage<TState>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Default implementations of storage handlers
|
|
336
|
+
*/
|
|
337
|
+
export const defaultBlockStorageHandlers: Required<BlockStorageHandlers<unknown>> = {
|
|
338
|
+
transformStateForStorage: <TState>(
|
|
339
|
+
storage: BlockStorage<TState>,
|
|
340
|
+
newState: TState,
|
|
341
|
+
): BlockStorage<TState> => updateStorageData(storage, { operation: 'update-data', value: newState }),
|
|
342
|
+
|
|
343
|
+
deriveStateForArgs: <TState>(storage: BlockStorage<TState>): TState => getStorageData(storage),
|
|
344
|
+
|
|
345
|
+
migrateStorage: <TState>(
|
|
346
|
+
storage: BlockStorage<TState>,
|
|
347
|
+
_fromVersion: number,
|
|
348
|
+
toVersion: number,
|
|
349
|
+
): BlockStorage<TState> => updateStorageDataVersion(storage, toVersion),
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Merges custom handlers with defaults
|
|
354
|
+
*
|
|
355
|
+
* @param customHandlers - Custom handlers to merge
|
|
356
|
+
* @returns Complete handlers with defaults for missing functions
|
|
357
|
+
*/
|
|
358
|
+
export function mergeBlockStorageHandlers<TState>(
|
|
359
|
+
customHandlers?: BlockStorageHandlers<TState>,
|
|
360
|
+
): Required<BlockStorageHandlers<TState>> {
|
|
361
|
+
return {
|
|
362
|
+
...defaultBlockStorageHandlers,
|
|
363
|
+
...customHandlers,
|
|
364
|
+
} as Required<BlockStorageHandlers<TState>>;
|
|
365
|
+
}
|