@platforma-sdk/model 1.54.10 → 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/bconfig/normalization.cjs +8 -1
  2. package/dist/bconfig/normalization.cjs.map +1 -1
  3. package/dist/bconfig/normalization.d.ts.map +1 -1
  4. package/dist/bconfig/normalization.js +8 -1
  5. package/dist/bconfig/normalization.js.map +1 -1
  6. package/dist/block_api_v3.d.ts +2 -2
  7. package/dist/block_api_v3.d.ts.map +1 -1
  8. package/dist/block_migrations.cjs +246 -214
  9. package/dist/block_migrations.cjs.map +1 -1
  10. package/dist/block_migrations.d.ts +180 -158
  11. package/dist/block_migrations.d.ts.map +1 -1
  12. package/dist/block_migrations.js +247 -214
  13. package/dist/block_migrations.js.map +1 -1
  14. package/dist/block_model.cjs +85 -35
  15. package/dist/block_model.cjs.map +1 -1
  16. package/dist/block_model.d.ts +66 -38
  17. package/dist/block_model.d.ts.map +1 -1
  18. package/dist/block_model.js +86 -36
  19. package/dist/block_model.js.map +1 -1
  20. package/dist/{builder.cjs → block_model_legacy.cjs} +2 -2
  21. package/dist/block_model_legacy.cjs.map +1 -0
  22. package/dist/{builder.d.ts → block_model_legacy.d.ts} +1 -1
  23. package/dist/block_model_legacy.d.ts.map +1 -0
  24. package/dist/{builder.js → block_model_legacy.js} +2 -2
  25. package/dist/block_model_legacy.js.map +1 -0
  26. package/dist/block_state_patch.d.ts +11 -1
  27. package/dist/block_state_patch.d.ts.map +1 -1
  28. package/dist/block_storage.cjs +126 -109
  29. package/dist/block_storage.cjs.map +1 -1
  30. package/dist/block_storage.d.ts +109 -112
  31. package/dist/block_storage.d.ts.map +1 -1
  32. package/dist/block_storage.js +126 -101
  33. package/dist/block_storage.js.map +1 -1
  34. package/dist/block_storage_callbacks.cjs +227 -0
  35. package/dist/block_storage_callbacks.cjs.map +1 -0
  36. package/dist/block_storage_callbacks.d.ts +113 -0
  37. package/dist/block_storage_callbacks.d.ts.map +1 -0
  38. package/dist/block_storage_callbacks.js +218 -0
  39. package/dist/block_storage_callbacks.js.map +1 -0
  40. package/dist/block_storage_facade.cjs +104 -0
  41. package/dist/block_storage_facade.cjs.map +1 -0
  42. package/dist/block_storage_facade.d.ts +168 -0
  43. package/dist/block_storage_facade.d.ts.map +1 -0
  44. package/dist/block_storage_facade.js +99 -0
  45. package/dist/block_storage_facade.js.map +1 -0
  46. package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
  47. package/dist/components/PlDataTable/state-migration.js.map +1 -1
  48. package/dist/components/PlDataTable/table.cjs +11 -2
  49. package/dist/components/PlDataTable/table.cjs.map +1 -1
  50. package/dist/components/PlDataTable/table.d.ts.map +1 -1
  51. package/dist/components/PlDataTable/table.js +12 -3
  52. package/dist/components/PlDataTable/table.js.map +1 -1
  53. package/dist/components/PlDataTable/v5.d.ts +7 -4
  54. package/dist/components/PlDataTable/v5.d.ts.map +1 -1
  55. package/dist/filters/converters/filterToQuery.cjs +3 -4
  56. package/dist/filters/converters/filterToQuery.cjs.map +1 -1
  57. package/dist/filters/converters/filterToQuery.d.ts +1 -1
  58. package/dist/filters/converters/filterToQuery.d.ts.map +1 -1
  59. package/dist/filters/converters/filterToQuery.js +3 -4
  60. package/dist/filters/converters/filterToQuery.js.map +1 -1
  61. package/dist/filters/distill.cjs.map +1 -1
  62. package/dist/filters/distill.d.ts +3 -2
  63. package/dist/filters/distill.d.ts.map +1 -1
  64. package/dist/filters/distill.js.map +1 -1
  65. package/dist/filters/traverse.cjs +7 -3
  66. package/dist/filters/traverse.cjs.map +1 -1
  67. package/dist/filters/traverse.d.ts +14 -12
  68. package/dist/filters/traverse.d.ts.map +1 -1
  69. package/dist/filters/traverse.js +7 -3
  70. package/dist/filters/traverse.js.map +1 -1
  71. package/dist/index.cjs +13 -14
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.d.ts +8 -3
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +6 -4
  76. package/dist/index.js.map +1 -1
  77. package/dist/package.json.cjs +1 -1
  78. package/dist/package.json.js +1 -1
  79. package/dist/platforma.d.ts +11 -4
  80. package/dist/platforma.d.ts.map +1 -1
  81. package/dist/plugin_model.cjs +171 -0
  82. package/dist/plugin_model.cjs.map +1 -0
  83. package/dist/plugin_model.d.ts +162 -0
  84. package/dist/plugin_model.d.ts.map +1 -0
  85. package/dist/plugin_model.js +169 -0
  86. package/dist/plugin_model.js.map +1 -0
  87. package/dist/render/api.cjs +20 -21
  88. package/dist/render/api.cjs.map +1 -1
  89. package/dist/render/api.d.ts +8 -8
  90. package/dist/render/api.d.ts.map +1 -1
  91. package/dist/render/api.js +20 -21
  92. package/dist/render/api.js.map +1 -1
  93. package/dist/render/internal.cjs.map +1 -1
  94. package/dist/render/internal.d.ts +1 -1
  95. package/dist/render/internal.d.ts.map +1 -1
  96. package/dist/render/internal.js.map +1 -1
  97. package/dist/version.cjs +4 -0
  98. package/dist/version.cjs.map +1 -1
  99. package/dist/version.d.ts +4 -0
  100. package/dist/version.d.ts.map +1 -1
  101. package/dist/version.js +4 -1
  102. package/dist/version.js.map +1 -1
  103. package/package.json +6 -6
  104. package/src/bconfig/normalization.ts +8 -1
  105. package/src/block_api_v3.ts +2 -2
  106. package/src/block_migrations.test.ts +141 -171
  107. package/src/block_migrations.ts +300 -285
  108. package/src/block_model.ts +205 -95
  109. package/src/{builder.ts → block_model_legacy.ts} +1 -1
  110. package/src/block_state_patch.ts +13 -1
  111. package/src/block_storage.test.ts +283 -95
  112. package/src/block_storage.ts +199 -188
  113. package/src/block_storage_callbacks.ts +326 -0
  114. package/src/block_storage_facade.ts +199 -0
  115. package/src/components/PlDataTable/state-migration.ts +4 -4
  116. package/src/components/PlDataTable/table.ts +16 -3
  117. package/src/components/PlDataTable/v5.ts +9 -5
  118. package/src/filters/converters/filterToQuery.ts +8 -7
  119. package/src/filters/distill.ts +19 -11
  120. package/src/filters/traverse.ts +44 -24
  121. package/src/index.ts +7 -3
  122. package/src/platforma.ts +26 -7
  123. package/src/plugin_model.test.ts +168 -0
  124. package/src/plugin_model.ts +242 -0
  125. package/src/render/api.ts +26 -24
  126. package/src/render/internal.ts +1 -1
  127. package/src/typing.test.ts +1 -1
  128. package/src/version.ts +8 -0
  129. package/dist/block_storage_vm.cjs +0 -262
  130. package/dist/block_storage_vm.cjs.map +0 -1
  131. package/dist/block_storage_vm.d.ts +0 -59
  132. package/dist/block_storage_vm.d.ts.map +0 -1
  133. package/dist/block_storage_vm.js +0 -258
  134. package/dist/block_storage_vm.js.map +0 -1
  135. package/dist/branding.d.ts +0 -7
  136. package/dist/branding.d.ts.map +0 -1
  137. package/dist/builder.cjs.map +0 -1
  138. package/dist/builder.d.ts.map +0 -1
  139. package/dist/builder.js.map +0 -1
  140. package/dist/sdk_info.cjs +0 -10
  141. package/dist/sdk_info.cjs.map +0 -1
  142. package/dist/sdk_info.d.ts +0 -5
  143. package/dist/sdk_info.d.ts.map +0 -1
  144. package/dist/sdk_info.js +0 -8
  145. package/dist/sdk_info.js.map +0 -1
  146. package/dist/unionize.d.ts +0 -12
  147. package/dist/unionize.d.ts.map +0 -1
  148. package/src/block_storage_vm.ts +0 -346
  149. package/src/branding.ts +0 -4
  150. package/src/sdk_info.ts +0 -9
  151. package/src/unionize.ts +0 -12
@@ -0,0 +1,326 @@
1
+ /**
2
+ * BlockStorage Callback Implementations - wired to facade callbacks in BlockModelV3._done().
3
+ *
4
+ * Provides pure functions for storage operations (migration, initialization,
5
+ * args derivation, updates, debug views). Each function takes its dependencies
6
+ * explicitly as parameters.
7
+ *
8
+ * @module block_storage_callbacks
9
+ * @internal
10
+ */
11
+
12
+ import {
13
+ BLOCK_STORAGE_KEY,
14
+ BLOCK_STORAGE_SCHEMA_VERSION,
15
+ type BlockStorage,
16
+ type MutateStoragePayload,
17
+ type StorageDebugView,
18
+ type PluginRegistry,
19
+ type VersionedData,
20
+ createBlockStorage,
21
+ getStorageData,
22
+ isBlockStorage,
23
+ migrateBlockStorage,
24
+ normalizeBlockStorage,
25
+ updateStorageData,
26
+ } from "./block_storage";
27
+
28
+ import { stringifyJson, type StringifiedJson } from "@milaboratories/pl-model-common";
29
+ import type { DataMigrationResult, DataVersioned } from "./block_migrations";
30
+
31
+ // =============================================================================
32
+ // Hook interfaces for dependency injection
33
+ // =============================================================================
34
+
35
+ /** Dependencies for storage migration */
36
+ export interface MigrationHooks {
37
+ migrateBlockData: (versioned: DataVersioned<unknown>) => DataMigrationResult<unknown>;
38
+ getPluginRegistry: () => PluginRegistry;
39
+ migratePluginData: (
40
+ pluginId: string,
41
+ versioned: DataVersioned<unknown>,
42
+ ) => DataMigrationResult<unknown> | undefined;
43
+ createPluginData: (pluginId: string) => DataVersioned<unknown>;
44
+ }
45
+
46
+ /** Dependencies for initial storage creation */
47
+ export interface InitialStorageHooks {
48
+ getDefaultBlockData: () => DataVersioned<unknown>;
49
+ getPluginRegistry: () => PluginRegistry;
50
+ createPluginData: (pluginId: string) => DataVersioned<unknown>;
51
+ }
52
+
53
+ /**
54
+ * Result of storage normalization
55
+ */
56
+ export interface NormalizeStorageResult {
57
+ /** The normalized BlockStorage object */
58
+ storage: BlockStorage;
59
+ /** The extracted data (what developers see) */
60
+ data: unknown;
61
+ }
62
+
63
+ /**
64
+ * Normalizes raw storage data and extracts state.
65
+ * Handles all formats:
66
+ * - New BlockStorage format (has discriminator)
67
+ * - Legacy V1/V2 format ({ args, uiState })
68
+ * - Raw V3 state (any other format)
69
+ *
70
+ * @param rawStorage - Raw data from blockStorage field (may be JSON string or object)
71
+ * @returns Object with normalized storage and extracted state
72
+ */
73
+ function normalizeStorage(rawStorage: unknown): NormalizeStorageResult {
74
+ // Handle undefined/null
75
+ if (rawStorage === undefined || rawStorage === null) {
76
+ const storage = createBlockStorage({});
77
+ return { storage, data: {} };
78
+ }
79
+
80
+ // Parse JSON string if needed
81
+ let parsed = rawStorage;
82
+ if (typeof rawStorage === "string") {
83
+ try {
84
+ parsed = JSON.parse(rawStorage);
85
+ } catch {
86
+ // If parsing fails, treat string as the data
87
+ const storage = createBlockStorage(rawStorage);
88
+ return { storage, data: rawStorage };
89
+ }
90
+ }
91
+
92
+ // Check for BlockStorage format (has discriminator)
93
+ if (isBlockStorage(parsed)) {
94
+ const storage = normalizeBlockStorage(parsed);
95
+ return { storage, data: getStorageData(storage) };
96
+ }
97
+
98
+ // Check for legacy V1/V2 format: { args, uiState }
99
+ if (isLegacyModelV1ApiFormat(parsed)) {
100
+ // For legacy format, the whole object IS the data
101
+ const storage = createBlockStorage(parsed);
102
+ return { storage, data: parsed };
103
+ }
104
+
105
+ // Raw V3 data - wrap it
106
+ const storage = createBlockStorage(parsed);
107
+ return { storage, data: parsed };
108
+ }
109
+
110
+ /**
111
+ * Applies a state update to existing storage.
112
+ * Used when setData is called from the frontend.
113
+ *
114
+ * @param currentStorageJson - Current storage as JSON string (must be defined)
115
+ * @param payload - Update payload with operation type and value
116
+ * @returns Updated storage as StringifiedJson<BlockStorage>
117
+ */
118
+ export function applyStorageUpdate(
119
+ currentStorageJson: string,
120
+ payload: MutateStoragePayload,
121
+ ): StringifiedJson<BlockStorage> {
122
+ const { storage: currentStorage } = normalizeStorage(currentStorageJson);
123
+
124
+ // Update data while preserving other storage fields (version, plugins)
125
+ const updatedStorage = updateStorageData(currentStorage, payload);
126
+
127
+ return stringifyJson(updatedStorage);
128
+ }
129
+
130
+ /**
131
+ * Checks if data is in legacy Model API v1 format.
132
+ * Legacy format has { args, uiState? } at top level without the BlockStorage discriminator.
133
+ */
134
+ function isLegacyModelV1ApiFormat(data: unknown): data is { args?: unknown } {
135
+ if (data === null || typeof data !== "object") return false;
136
+ if (isBlockStorage(data)) return false;
137
+
138
+ const obj = data as Record<string, unknown>;
139
+ return "args" in obj;
140
+ }
141
+
142
+ // =============================================================================
143
+ // Facade Callback Implementations
144
+ // =============================================================================
145
+
146
+ /**
147
+ * Gets storage debug view from raw storage data.
148
+ * Returns structured debug info about the storage state.
149
+ *
150
+ * @param rawStorage - Raw data from blockStorage field (may be JSON string or object)
151
+ * @returns JSON string with storage debug view
152
+ */
153
+ export function getStorageDebugView(rawStorage: unknown): StringifiedJson<StorageDebugView> {
154
+ const { storage } = normalizeStorage(rawStorage);
155
+ const debugView: StorageDebugView = {
156
+ dataVersion: storage.__dataVersion,
157
+ data: storage.__data,
158
+ };
159
+ return stringifyJson(debugView);
160
+ }
161
+
162
+ // =============================================================================
163
+ // Migration Support
164
+ // =============================================================================
165
+
166
+ /**
167
+ * Result of storage migration.
168
+ * Returned by __pl_storage_migrate callback.
169
+ *
170
+ * - Error result: { error: string } - serious failure (no context, etc.)
171
+ * - Success result: { newStorageJson: StringifiedJson<BlockStorage>, info: string } - migration succeeded
172
+ */
173
+ export type MigrationResult =
174
+ | { error: string }
175
+ | { error?: undefined; newStorageJson: StringifiedJson<BlockStorage>; info: string };
176
+
177
+ /**
178
+ * Runs storage migration using the provided hooks.
179
+ * This is the main entry point for the middle layer to trigger migrations.
180
+ *
181
+ * @param currentStorageJson - Current storage as JSON string (or undefined)
182
+ * @param hooks - Migration dependencies (block/plugin data migration and creation functions)
183
+ * @returns MigrationResult
184
+ */
185
+ export function migrateStorage(
186
+ currentStorageJson: string | undefined,
187
+ hooks: MigrationHooks,
188
+ ): MigrationResult {
189
+ // Normalize current storage
190
+ const { storage: currentStorage } = normalizeStorage(currentStorageJson);
191
+
192
+ const newPluginRegistry = hooks.getPluginRegistry();
193
+
194
+ // Perform atomic migration of block + all plugins
195
+ const migrationResult = migrateBlockStorage(currentStorage, {
196
+ migrateBlockData: hooks.migrateBlockData,
197
+ migratePluginData: hooks.migratePluginData,
198
+ newPluginRegistry,
199
+ createPluginData: hooks.createPluginData,
200
+ });
201
+
202
+ if (!migrationResult.success) {
203
+ return {
204
+ error: `Migration failed at '${migrationResult.failedAt}': ${migrationResult.error}`,
205
+ };
206
+ }
207
+
208
+ // Build info message
209
+ const oldVersion = currentStorage.__dataVersion;
210
+ const newVersion = migrationResult.storage.__dataVersion;
211
+ const info =
212
+ oldVersion === newVersion
213
+ ? `No migration needed (${oldVersion})`
214
+ : `Migrated ${oldVersion} -> ${newVersion}`;
215
+
216
+ return {
217
+ newStorageJson: stringifyJson(migrationResult.storage),
218
+ info,
219
+ };
220
+ }
221
+
222
+ // =============================================================================
223
+ // Initial Storage Creation
224
+ // =============================================================================
225
+
226
+ /**
227
+ * Creates complete initial storage (block data + all plugin data) atomically.
228
+ *
229
+ * @param hooks - Dependencies for creating initial block and plugin data
230
+ * @returns Initial storage as branded JSON string
231
+ */
232
+ export function createInitialStorage(hooks: InitialStorageHooks): StringifiedJson<BlockStorage> {
233
+ const blockDefault = hooks.getDefaultBlockData();
234
+ const pluginRegistry = hooks.getPluginRegistry();
235
+
236
+ const plugins: Record<string, VersionedData<unknown>> = {};
237
+ for (const pluginId of Object.keys(pluginRegistry)) {
238
+ const initial = hooks.createPluginData(pluginId);
239
+ plugins[pluginId] = { __dataVersion: initial.version, __data: initial.data };
240
+ }
241
+
242
+ const storage: BlockStorage = {
243
+ [BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
244
+ __dataVersion: blockDefault.version,
245
+ __data: blockDefault.data,
246
+ __pluginRegistry: pluginRegistry,
247
+ __plugins: plugins,
248
+ };
249
+ return stringifyJson(storage);
250
+ }
251
+
252
+ // =============================================================================
253
+ // Args Derivation from Storage
254
+ // =============================================================================
255
+
256
+ /**
257
+ * Result of args derivation from storage.
258
+ * Returned by __pl_args_derive and __pl_prerunArgs_derive callbacks.
259
+ */
260
+ export type ArgsDeriveResult = { error: string } | { error?: undefined; value: unknown };
261
+
262
+ /**
263
+ * Derives args from storage using the provided args function.
264
+ * This extracts data from storage and passes it to the block's args() function.
265
+ *
266
+ * @param storageJson - Storage as JSON string
267
+ * @param argsFunction - The block's args derivation function
268
+ * @returns ArgsDeriveResult with derived args or error
269
+ */
270
+ export function deriveArgsFromStorage(
271
+ storageJson: string,
272
+ argsFunction: (data: unknown) => unknown,
273
+ ): ArgsDeriveResult {
274
+ // Extract data from storage
275
+ const { data } = normalizeStorage(storageJson);
276
+
277
+ // Call the args function with extracted data
278
+ try {
279
+ const result = argsFunction(data);
280
+ return { value: result };
281
+ } catch (e) {
282
+ const errorMsg = e instanceof Error ? e.message : String(e);
283
+ return { error: `args() threw: ${errorMsg}` };
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Derives prerunArgs from storage.
289
+ * Uses prerunArgsFunction if provided, otherwise falls back to argsFunction.
290
+ *
291
+ * @param storageJson - Storage as JSON string
292
+ * @param argsFunction - The block's args derivation function (fallback)
293
+ * @param prerunArgsFunction - Optional prerun args derivation function
294
+ * @returns ArgsDeriveResult with derived prerunArgs or error
295
+ */
296
+ export function derivePrerunArgsFromStorage(
297
+ storageJson: string,
298
+ argsFunction: (data: unknown) => unknown,
299
+ prerunArgsFunction?: (data: unknown) => unknown,
300
+ ): ArgsDeriveResult {
301
+ // Extract data from storage
302
+ const { data } = normalizeStorage(storageJson);
303
+
304
+ // Try prerunArgs function first if available
305
+ if (prerunArgsFunction) {
306
+ try {
307
+ const result = prerunArgsFunction(data);
308
+ return { value: result };
309
+ } catch (e) {
310
+ const errorMsg = e instanceof Error ? e.message : String(e);
311
+ return { error: `prerunArgs() threw: ${errorMsg}` };
312
+ }
313
+ }
314
+
315
+ // Fall back to args function
316
+ try {
317
+ const result = argsFunction(data);
318
+ return { value: result };
319
+ } catch (e) {
320
+ const errorMsg = e instanceof Error ? e.message : String(e);
321
+ return { error: `args() threw (fallback): ${errorMsg}` };
322
+ }
323
+ }
324
+
325
+ // Export discriminator key and schema version for external checks
326
+ export { BLOCK_STORAGE_KEY, BLOCK_STORAGE_SCHEMA_VERSION };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Block Storage Facade - Contract between bundled blocks and middle layer.
3
+ *
4
+ * ============================================================================
5
+ * VERSIONING
6
+ * ============================================================================
7
+ *
8
+ * Blocks declare their model API version via the `requiresModelAPIVersion` feature flag
9
+ * (see BlockCodeKnownFeatureFlags). This determines how the middle layer manages block state:
10
+ *
11
+ * - Version 1: Legacy BlockModel - state is {args, uiState}, managed directly by middle layer
12
+ * - Version 2: BlockModelV3 - uses blockStorage with VM-based callbacks (this facade)
13
+ *
14
+ * This facade (BlockStorageFacade) is used by blocks with `requiresModelAPIVersion: 2`.
15
+ * The version number matches the model API version for clarity.
16
+ *
17
+ * ============================================================================
18
+ * BACKWARD COMPATIBILITY WARNING
19
+ * ============================================================================
20
+ *
21
+ * This file documents the FACADE between the SDK (bundled into blocks) and the
22
+ * middle layer. Once a block is published, its SDK version is frozen. The middle
23
+ * layer must support ALL previously released callback signatures indefinitely.
24
+ *
25
+ * RULES:
26
+ * 1. NEVER change the signature of existing callbacks
27
+ * 2. NEVER remove existing callbacks
28
+ * 3. New callbacks CAN be added (old blocks won't register them, middle layer
29
+ * should handle missing callbacks gracefully)
30
+ * 4. Callback return types can be EXTENDED (add optional fields) but not changed
31
+ * 5. Callback parameter types should remain compatible (middle layer may need
32
+ * to handle both old and new formats)
33
+ *
34
+ * The facade consists of callbacks registered via `tryRegisterCallback()` with
35
+ * the `__pl_` prefix. These are registered by the SDK when a block loads and
36
+ * called by the middle layer to perform operations.
37
+ *
38
+ * ============================================================================
39
+ * WHAT CAN BE CHANGED FREELY
40
+ * ============================================================================
41
+ *
42
+ * - Middle layer code (lib/node/pl-middle-layer)
43
+ * - SDK internal implementation (as long as callback contracts are preserved)
44
+ * - SDK exports used ONLY by middle layer (not by blocks themselves)
45
+ * - New SDK features that don't affect existing callbacks
46
+ *
47
+ * @module block_storage_facade
48
+ */
49
+
50
+ import type { MutateStoragePayload } from "./block_storage";
51
+ import type { ConfigRenderLambda } from "./bconfig";
52
+ import { createRenderLambda, tryRegisterCallback } from "./internal";
53
+ import type { StringifiedJson } from "@milaboratories/pl-model-common";
54
+
55
+ // =============================================================================
56
+ // Facade Version
57
+ // =============================================================================
58
+
59
+ /**
60
+ * The current facade version. This value is used for `requiresModelAPIVersion`
61
+ * feature flag in BlockModelV3.
62
+ */
63
+ export const BLOCK_STORAGE_FACADE_VERSION = 2;
64
+
65
+ // =============================================================================
66
+ // Facade Callback Names
67
+ // =============================================================================
68
+
69
+ /**
70
+ * All facade callback names as constants.
71
+ * These are the source of truth - the interface is derived from these.
72
+ *
73
+ * IMPORTANT: When adding a new callback:
74
+ * 1. Add the constant here
75
+ * 2. Add the callback signature to FacadeCallbackTypes below
76
+ * 3. The BlockStorageFacade type will automatically include it
77
+ */
78
+ export const BlockStorageFacadeCallbacks = {
79
+ StorageApplyUpdate: "__pl_storage_applyUpdate",
80
+ StorageDebugView: "__pl_storage_debugView",
81
+ StorageMigrate: "__pl_storage_migrate",
82
+ ArgsDerive: "__pl_args_derive",
83
+ PrerunArgsDerive: "__pl_prerunArgs_derive",
84
+ StorageInitial: "__pl_storage_initial",
85
+ } as const;
86
+
87
+ /**
88
+ * Creates a map of lambda handles from a callbacks constant object.
89
+ * Keys are the callback string values (e.g., '__pl_storage_applyUpdate').
90
+ */
91
+ function createFacadeHandles<T extends Record<string, string>>(
92
+ callbacks: T,
93
+ ): { [K in T[keyof T]]: ConfigRenderLambda } {
94
+ return Object.fromEntries(
95
+ Object.values(callbacks).map((handle) => [handle, createRenderLambda({ handle })]),
96
+ ) as { [K in T[keyof T]]: ConfigRenderLambda };
97
+ }
98
+
99
+ /**
100
+ * Lambda handles for facade callbacks.
101
+ * Used by the middle layer to invoke callbacks via executeSingleLambda().
102
+ */
103
+ export const BlockStorageFacadeHandles = createFacadeHandles(BlockStorageFacadeCallbacks);
104
+
105
+ // =============================================================================
106
+ // Facade Interface (source of truth for callback signatures)
107
+ // =============================================================================
108
+
109
+ /**
110
+ * The complete facade interface between bundled blocks (SDK) and middle layer.
111
+ *
112
+ * This interface defines ALL callbacks that a block registers. The middle layer
113
+ * calls these callbacks to perform storage operations.
114
+ *
115
+ * ALL types are inlined to simplify versioning - when a callback changes,
116
+ * the entire signature is visible in one place.
117
+ *
118
+ * BACKWARD COMPATIBILITY:
119
+ * - This interface can only be EXTENDED, never shrunk
120
+ * - Existing callback signatures MUST NOT change
121
+ * - Middle layer should use Partial<BlockStorageFacade> when dealing with
122
+ * blocks of unknown version (older blocks may not have all callbacks)
123
+ *
124
+ * Each callback is documented with:
125
+ * - Purpose and when it's called
126
+ * - Parameter descriptions
127
+ * - Return value description
128
+ */
129
+ export interface BlockStorageFacade {
130
+ /**
131
+ * Apply state update to storage.
132
+ * Called when UI updates block state (setState) or plugin data.
133
+ * @param currentStorageJson - Current storage as JSON string
134
+ * @param payload - Update payload with operation type and value
135
+ * @returns Updated storage as JSON string
136
+ */
137
+ [BlockStorageFacadeCallbacks.StorageApplyUpdate]: (
138
+ currentStorageJson: StringifiedJson,
139
+ payload: MutateStoragePayload,
140
+ ) => StringifiedJson;
141
+
142
+ /**
143
+ * Get debug view of storage.
144
+ * Called by developer tools to inspect storage state.
145
+ * @param storageJson - Storage as JSON string (or undefined for new blocks)
146
+ * @returns JSON string containing StorageDebugView
147
+ */
148
+ [BlockStorageFacadeCallbacks.StorageDebugView]: (
149
+ storageJson: StringifiedJson | undefined,
150
+ ) => StringifiedJson;
151
+
152
+ /**
153
+ * Run storage migration.
154
+ * Called when block loads to migrate data to latest version.
155
+ * @param currentStorageJson - Current storage as JSON string (or undefined for new blocks)
156
+ * @returns Migration result - either error or success with new storage
157
+ */
158
+ [BlockStorageFacadeCallbacks.StorageMigrate]: (currentStorageJson: StringifiedJson | undefined) =>
159
+ | { error: string }
160
+ | {
161
+ error?: undefined;
162
+ newStorageJson: StringifiedJson;
163
+ info: string;
164
+ };
165
+
166
+ /**
167
+ * Derive args from storage.
168
+ * Called to get block configuration args from storage.
169
+ * @param storageJson - Storage as JSON string
170
+ * @returns Args derivation result - either error or derived value
171
+ */
172
+ [BlockStorageFacadeCallbacks.ArgsDerive]: (
173
+ storageJson: StringifiedJson,
174
+ ) => { error: string } | { error?: undefined; value: unknown };
175
+
176
+ /**
177
+ * Derive prerunArgs from storage.
178
+ * Called to get prerun args; falls back to args callback if not registered.
179
+ * @param storageJson - Storage as JSON string
180
+ * @returns Args derivation result - either error or derived value
181
+ */
182
+ [BlockStorageFacadeCallbacks.PrerunArgsDerive]: (
183
+ storageJson: StringifiedJson,
184
+ ) => { error: string } | { error?: undefined; value: unknown };
185
+
186
+ /**
187
+ * Get initial storage JSON for new blocks.
188
+ * Called when creating a new block to get complete initial storage.
189
+ * @returns Initial storage as JSON string
190
+ */
191
+ [BlockStorageFacadeCallbacks.StorageInitial]: () => StringifiedJson;
192
+ }
193
+
194
+ /** Register all facade callbacks at once. Ensures all required callbacks are provided. */
195
+ export function registerFacadeCallbacks(callbacks: BlockStorageFacade): void {
196
+ for (const key of Object.values(BlockStorageFacadeCallbacks)) {
197
+ tryRegisterCallback(key, callbacks[key] as (...args: any[]) => any);
198
+ }
199
+ }
@@ -158,10 +158,10 @@ function migrateV4toV5(
158
158
  const nextId = () => ++idCounter;
159
159
 
160
160
  const migratedCache: PlDataTableStateV2CacheEntry[] = state.stateCache.map((entry) => {
161
- const leaves: PlDataTableFiltersWithMeta[] = [];
161
+ const leaves: PlDataTableFiltersWithMeta["filters"] = [];
162
162
  for (const f of entry.filtersState) {
163
163
  if (f.filter !== null && !f.filter.disabled) {
164
- const column = canonicalizeJson<PTableColumnId>(f.id);
164
+ const column = canonicalizeJson(f.id);
165
165
  leaves.push(migrateTableFilter(column, f.filter.value, nextId));
166
166
  }
167
167
  }
@@ -198,10 +198,10 @@ function migrateV4toV5(
198
198
 
199
199
  /** Migrate a single per-column PlTableFilter to a tree-based FilterSpec node */
200
200
  function migrateTableFilter(
201
- column: string,
201
+ column: CanonicalizedJson<PTableColumnId>,
202
202
  filter: PlTableFilter,
203
203
  nextId: () => number,
204
- ): PlDataTableFiltersWithMeta {
204
+ ): PlDataTableFiltersWithMeta["filters"][number] {
205
205
  const id = nextId();
206
206
  switch (filter.type) {
207
207
  case "isNA":
@@ -26,6 +26,7 @@ import {
26
26
  readAnnotation,
27
27
  uniqueBy,
28
28
  isBooleanExpression,
29
+ parseJson,
29
30
  } from "@milaboratories/pl-model-common";
30
31
  import { filterSpecToSpecQueryExpr } from "../../filters";
31
32
  import type { RenderCtxBase, TreeNodeAccessor, PColumnDataUniversal } from "../../render";
@@ -186,7 +187,7 @@ export function createPlDataTableV2<A, U>(
186
187
  const stateFilters = tableStateNormalized.pTableParams.filters;
187
188
  const opsFilters = ops?.filters ?? null;
188
189
  const filters: null | PlDataTableFilters =
189
- stateFilters !== null && opsFilters !== null
190
+ stateFilters != null && opsFilters != null
190
191
  ? { type: "and", filters: [stateFilters, opsFilters] }
191
192
  : (stateFilters ?? opsFilters);
192
193
  const filterColumns = filters ? collectFilterSpecColumns(filters) : [];
@@ -218,6 +219,7 @@ export function createPlDataTableV2<A, U>(
218
219
  sorting,
219
220
  coreColumnPredicate: ops?.coreColumnPredicate,
220
221
  });
222
+
221
223
  const fullHandle = ctx.createPTableV2(fullDef);
222
224
  if (!fullHandle) return undefined;
223
225
 
@@ -245,12 +247,22 @@ export function createPlDataTableV2<A, U>(
245
247
  coreColumns.forEach((c) => hiddenColumns.delete(c));
246
248
  }
247
249
 
248
- // Sorting changes the order of result rows — preserve sorted columns from being hidden
250
+ // Preserve sorted columns from being hidden
249
251
  sorting
250
252
  .map((s) => s.column)
251
253
  .filter((c): c is PTableColumnIdColumn => c.type === "column")
252
254
  .forEach((c) => hiddenColumns.delete(c.id));
253
255
 
256
+ // Preserve filter columns from being hidden
257
+ if (filters) {
258
+ collectFilterSpecColumns(filters)
259
+ .flatMap((c) => {
260
+ const obj = parseJson(c);
261
+ return obj.type === "column" ? [obj.id] : [];
262
+ })
263
+ .forEach((c) => hiddenColumns.delete(c));
264
+ }
265
+
254
266
  const visibleColumns = columns.filter((c) => !hiddenColumns.has(c.id));
255
267
  const visibleLabelColumns = getMatchingLabelColumns(
256
268
  visibleColumns.map(getColumnIdAndSpec),
@@ -269,13 +281,14 @@ export function createPlDataTableV2<A, U>(
269
281
  coreColumnPredicate,
270
282
  });
271
283
  const visibleHandle = ctx.createPTableV2(visibleDef);
284
+
272
285
  if (!visibleHandle) return undefined;
273
286
 
274
287
  return {
275
288
  sourceId: tableStateNormalized.pTableParams.sourceId,
276
289
  fullTableHandle: fullHandle,
277
290
  visibleTableHandle: visibleHandle,
278
- } satisfies PlDataTableModel;
291
+ } as PlDataTableModel;
279
292
  }
280
293
 
281
294
  /** Create sheet entries for PlDataTable */
@@ -8,8 +8,10 @@ import type {
8
8
  PTableSorting,
9
9
  PColumnIdAndSpec,
10
10
  PTableHandle,
11
+ RootFilterSpec,
12
+ PTableColumnId,
11
13
  } from "@milaboratories/pl-model-common";
12
- import type { FilterSpec, FilterSpecLeaf } from "../../filters";
14
+ import type { FilterSpecLeaf } from "../../filters";
13
15
 
14
16
  export type PlTableColumnId = {
15
17
  /** Original column spec */
@@ -60,10 +62,10 @@ export type PlDataTableSheetState = {
60
62
  };
61
63
 
62
64
  /** Tree-based filter state compatible with PlAdvancedFilter's RootFilter */
63
- export type PlDataTableFilters = FilterSpec<FilterSpecLeaf<string>>;
64
- export type PlDataTableFiltersWithMeta = FilterSpec<
65
- FilterSpecLeaf<string>,
66
- { id: number; isExpanded?: boolean }
65
+ export type PlDataTableFilters = RootFilterSpec<FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>>;
66
+ export type PlDataTableFiltersWithMeta = RootFilterSpec<
67
+ FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>,
68
+ { id: number; isExpanded?: boolean; source?: "table-filter" | "table-search" }
67
69
  >;
68
70
 
69
71
  export type PlDataTableStateV2CacheEntry = {
@@ -75,6 +77,8 @@ export type PlDataTableStateV2CacheEntry = {
75
77
  sheetsState: PlDataTableSheetState[];
76
78
  /** Filters state (tree-based, compatible with PlAdvancedFilter) */
77
79
  filtersState: null | PlDataTableFiltersWithMeta;
80
+ /** Fast search string */
81
+ searchString?: string;
78
82
  };
79
83
 
80
84
  export type PTableParamsV2 =
@@ -11,15 +11,14 @@ import { traverseFilterSpec } from "../traverse";
11
11
  /** Parses a CanonicalizedJson<PTableColumnId> string into a SpecQueryExpression reference. */
12
12
  function resolveColumnRef(columnStr: string): SpecQueryExpression {
13
13
  const parsed = JSON.parse(columnStr) as PTableColumnId;
14
- if (parsed.type === "axis") {
15
- return { type: "axisRef", value: parsed.id as SingleAxisSelector };
16
- }
17
- return { type: "columnRef", value: parsed.id };
14
+ return parsed.type === "axis"
15
+ ? { type: "axisRef", value: parsed.id as SingleAxisSelector }
16
+ : { type: "columnRef", value: parsed.id };
18
17
  }
19
18
 
20
19
  /** Converts a FilterSpec tree into a SpecQueryExpression. */
21
- export function filterSpecToSpecQueryExpr(
22
- filter: FilterSpec<FilterSpecLeaf<string>>,
20
+ export function filterSpecToSpecQueryExpr<Leaf extends FilterSpecLeaf<string>>(
21
+ filter: FilterSpec<Leaf>,
23
22
  ): SpecQueryExpression {
24
23
  return traverseFilterSpec(filter, {
25
24
  leaf: leafToSpecQueryExpr,
@@ -39,7 +38,9 @@ export function filterSpecToSpecQueryExpr(
39
38
  });
40
39
  }
41
40
 
42
- function leafToSpecQueryExpr(filter: FilterSpecLeaf<string>): SpecQueryExpression {
41
+ function leafToSpecQueryExpr<Leaf extends FilterSpecLeaf<string>>(
42
+ filter: Leaf,
43
+ ): SpecQueryExpression {
43
44
  switch (filter.type) {
44
45
  case "patternEquals":
45
46
  return {