@platforma-sdk/model 1.75.5 → 1.75.10

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 (37) hide show
  1. package/dist/block_model.cjs +3 -3
  2. package/dist/block_model.cjs.map +1 -1
  3. package/dist/block_model.d.ts +4 -5
  4. package/dist/block_model.d.ts.map +1 -1
  5. package/dist/block_model.js +3 -3
  6. package/dist/block_model.js.map +1 -1
  7. package/dist/block_model_legacy.cjs +2 -1
  8. package/dist/block_model_legacy.cjs.map +1 -1
  9. package/dist/block_model_legacy.js +2 -1
  10. package/dist/block_model_legacy.js.map +1 -1
  11. package/dist/index.d.ts +3 -3
  12. package/dist/labels/derive_distinct_labels.cjs +58 -23
  13. package/dist/labels/derive_distinct_labels.cjs.map +1 -1
  14. package/dist/labels/derive_distinct_labels.js +58 -23
  15. package/dist/labels/derive_distinct_labels.js.map +1 -1
  16. package/dist/package.cjs +1 -1
  17. package/dist/package.js +1 -1
  18. package/dist/platforma.d.ts +8 -5
  19. package/dist/platforma.d.ts.map +1 -1
  20. package/dist/plugin_handle.cjs.map +1 -1
  21. package/dist/plugin_handle.d.ts +1 -1
  22. package/dist/plugin_handle.js.map +1 -1
  23. package/dist/plugin_model.cjs +34 -37
  24. package/dist/plugin_model.cjs.map +1 -1
  25. package/dist/plugin_model.d.ts +55 -53
  26. package/dist/plugin_model.d.ts.map +1 -1
  27. package/dist/plugin_model.js +34 -37
  28. package/dist/plugin_model.js.map +1 -1
  29. package/package.json +8 -8
  30. package/src/block_model.ts +16 -5
  31. package/src/block_model_legacy.ts +1 -0
  32. package/src/index.ts +2 -0
  33. package/src/labels/derive_distinct_labels.test.ts +22 -0
  34. package/src/labels/derive_distinct_labels.ts +101 -31
  35. package/src/platforma.ts +11 -5
  36. package/src/plugin_handle.ts +1 -1
  37. package/src/plugin_model.ts +189 -76
@@ -1 +1 @@
1
- {"version":3,"file":"plugin_model.js","names":[],"sources":["../src/plugin_model.ts"],"sourcesContent":["/**\n * PluginModel - Builder for creating plugin types with data model and outputs.\n *\n * Plugins are UI components with their own model logic and persistent state.\n * Block developers register plugin instances via BlockModelV3.plugin() method.\n *\n * @module plugin_model\n */\n\nimport type { BlockCodeKnownFeatureFlags, OutputWithStatus } from \"@milaboratories/pl-model-common\";\nimport type { ResolveModelServices, ResolveUiServices } from \"./services/service_resolve\";\nimport {\n type DataModel,\n DataModelBuilder,\n type DataRecoverFn,\n type DataVersioned,\n type TransferTarget,\n} from \"./block_migrations\";\nimport { type PluginName, DATA_MODEL_LEGACY_VERSION } from \"./block_storage\";\nimport type { PluginFactoryLike } from \"./plugin_handle\";\nimport type { PluginRenderCtx } from \"./render\";\n\n/** Symbol for internal builder creation method */\nconst FROM_BUILDER = Symbol(\"fromBuilder\");\n\n/** Output function signature for plugin render context. */\ntype PluginOutputFn<\n Data extends PluginData,\n Params extends PluginParams,\n ModelServices,\n UiServices,\n T,\n> = (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n) => T;\n\n/** Mapped output functions for a plugin's outputs. */\ntype PluginOutputFns<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n ModelServices,\n UiServices,\n> = {\n [K in keyof Outputs]: PluginOutputFn<Data, Params, ModelServices, UiServices, Outputs[K]>;\n};\n\n/** Symbol for internal plugin model creation — not accessible to external consumers */\nexport const CREATE_PLUGIN_MODEL = Symbol(\"createPluginModel\");\n\n/** Sentinel for PluginInstance without transferAt — no transfer possible. */\nconst NO_TRANSFER_VERSION = \"\";\n\nexport type PluginData = Record<string, unknown>;\nexport type PluginParams = undefined | Record<string, unknown>;\nexport type PluginOutputs = Record<string, unknown>;\nexport type PluginConfig = undefined | Record<string, unknown>;\n\n/**\n * Plugin data model with typed migration chain and config-aware initialization.\n *\n * @typeParam Data - Current (latest) plugin data type\n * @typeParam Versions - Map of version keys to their data types (accumulated by the chain)\n * @typeParam Config - Config type passed to init function (undefined if none)\n */\nexport class PluginDataModel<\n Data extends PluginData,\n Versions extends Record<string, unknown> = {},\n Config = undefined,\n> {\n readonly dataModel: DataModel<Data>;\n private readonly configInitFn: (config?: Config) => Data;\n\n /** @internal Phantom field to anchor the Versions type parameter. */\n declare readonly __versions?: Versions;\n\n private constructor(dataModel: DataModel<Data>, configInitFn: (config?: Config) => Data) {\n this.dataModel = dataModel;\n this.configInitFn = configInitFn;\n }\n\n /** @internal */\n static [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>, Config>(\n dataModel: DataModel<Data>,\n configInitFn: (config?: Config) => Data,\n ): PluginDataModel<Data, Versions, Config> {\n return new PluginDataModel<Data, Versions, Config>(dataModel, configInitFn);\n }\n\n /** Create fresh data with optional config. */\n getDefaultData(config?: Config): DataVersioned<Data> {\n const data = this.configInitFn(config);\n return { version: this.dataModel.version, data };\n }\n}\n\n/** Internal state for plugin data model chain. */\ntype PluginChainState = {\n initialVersion: string;\n migrations: Array<{ toVersion: string; fn: (data: unknown) => unknown }>;\n recoverFn?: (version: string, data: unknown) => unknown;\n recoverAtIndex?: number;\n};\n\n/** Version → persisted data shape for each migration step (third generic of `PluginModel.define` when `data` is a `PluginDataModel`). */\nexport type PluginDataModelVersions<\n M extends PluginDataModel<PluginData, Record<string, unknown>, unknown>,\n> = M extends PluginDataModel<PluginData, infer Versions, unknown> ? Versions : never;\n\n/**\n * Builder for creating PluginDataModel with type-safe migrations.\n * Mirrors DataModelBuilder — same .from(), .migrate(), .recover(), .init() chain.\n *\n * @example\n * const pluginData = new PluginDataModelBuilder()\n * .from<TableData>(\"v1\")\n * .migrate<FilteredTableData>(\"v2\", (v1) => ({ ...v1, filters: [] }))\n * .init<TableConfig>((config?) => ({\n * state: createDefaultState(config?.ops),\n * filters: [],\n * }));\n */\nexport class PluginDataModelBuilder {\n from<Data extends PluginData, const V extends string>(\n version: V,\n ): PluginDataModelInitialChain<Data, Record<V, Data>> {\n return PluginDataModelInitialChain[FROM_BUILDER]<Data, Record<V, Data>>({\n initialVersion: version,\n migrations: [],\n });\n }\n}\n\n/**\n * Chain returned by .migrate(). Supports .migrate(), .recover(), .init().\n * No .upgradeLegacy() — that is only available on the initial chain.\n */\nexport class PluginDataModelChain<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n> {\n protected constructor(protected readonly state: PluginChainState) {}\n\n /** @internal */\n static [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>>(\n state: PluginChainState,\n ): PluginDataModelChain<Data, Versions> {\n return new PluginDataModelChain(state);\n }\n\n /**\n * Add a migration step transforming data from the current version to the next.\n */\n migrate<Next extends PluginData, const NextV extends string>(\n version: NextV,\n fn: (current: Data) => Next,\n ): PluginDataModelChain<Next, Versions & Record<NextV, Next>> {\n return PluginDataModelChain[FROM_BUILDER]<Next, Versions & Record<NextV, Next>>({\n ...this.state,\n migrations: [\n ...this.state.migrations,\n { toVersion: version, fn: fn as (data: unknown) => unknown },\n ],\n });\n }\n\n /**\n * Set a recovery handler for unknown or legacy versions.\n * Can only be called once — the returned chain has no recover() method.\n */\n recover(fn: DataRecoverFn<Data>): PluginDataModelWithRecover<Data, Versions> {\n return PluginDataModelWithRecover[FROM_BUILDER]<Data, Versions>({\n ...this.state,\n recoverFn: fn as (version: string, data: unknown) => unknown,\n recoverAtIndex: this.state.migrations.length,\n });\n }\n\n /** Finalize the PluginDataModel. */\n init<Config = undefined>(fn: (config?: Config) => Data): PluginDataModel<Data, Versions, Config> {\n return buildPluginDataModel(this.state, fn);\n }\n}\n\n/**\n * Initial chain returned by new PluginDataModelBuilder().from().\n * Extends PluginDataModelChain with .upgradeLegacy() — available only before\n * any .migrate() calls, matching the block's DataModelInitialChain pattern.\n */\nexport class PluginDataModelInitialChain<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n> extends PluginDataModelChain<Data, Versions> {\n /** @internal */\n static override [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>>(\n state: PluginChainState,\n ): PluginDataModelInitialChain<Data, Versions> {\n return new PluginDataModelInitialChain(state);\n }\n\n /**\n * Handle data from a previous plugin type occupying this slot.\n * Prepends a migration from DATA_MODEL_LEGACY_VERSION to the initial version.\n * When a plugin type changes, the old data arrives with DATA_MODEL_LEGACY_VERSION —\n * this function transforms it into the initial version, then the normal chain runs.\n *\n * Must be called right after .from() — not available after .migrate().\n * Mutually exclusive with recover().\n *\n * @param fn - Transform from old plugin's raw data to this plugin's initial data type\n */\n upgradeLegacy(fn: (data: unknown) => Data): PluginDataModelWithRecover<Data, Versions> {\n return PluginDataModelWithRecover[FROM_BUILDER]<Data, Versions>({\n ...this.state,\n migrations: [\n { toVersion: this.state.initialVersion, fn: fn as (data: unknown) => unknown },\n ...this.state.migrations,\n ],\n initialVersion: DATA_MODEL_LEGACY_VERSION,\n });\n }\n}\n\n/**\n * Chain after .recover() — supports .migrate(), .init(). No second recover().\n */\nexport class PluginDataModelWithRecover<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n> {\n private constructor(private readonly state: PluginChainState) {}\n\n /** @internal */\n static [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>>(\n state: PluginChainState,\n ): PluginDataModelWithRecover<Data, Versions> {\n return new PluginDataModelWithRecover(state);\n }\n\n migrate<Next extends PluginData, const NextV extends string>(\n version: NextV,\n fn: (current: Data) => Next,\n ): PluginDataModelWithRecover<Next, Versions & Record<NextV, Next>> {\n return PluginDataModelWithRecover[FROM_BUILDER]<Next, Versions & Record<NextV, Next>>({\n ...this.state,\n migrations: [\n ...this.state.migrations,\n { toVersion: version, fn: fn as (data: unknown) => unknown },\n ],\n });\n }\n\n init<Config = undefined>(fn: (config?: Config) => Data): PluginDataModel<Data, Versions, Config> {\n return buildPluginDataModel(this.state, fn);\n }\n}\n\n/**\n * Builds a PluginDataModel by replaying the stored chain state through DataModelBuilder.\n * @internal\n */\nfunction buildPluginDataModel<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n Config,\n>(\n state: PluginChainState,\n configInitFn: (config?: Config) => Data,\n): PluginDataModel<Data, Versions, Config> {\n // Build inner DataModel by replaying migrations through DataModelBuilder chain.\n // Uses `any` internally — type safety is maintained by the public chain types.\n let chain: any = new DataModelBuilder().from(state.initialVersion);\n\n for (let i = 0; i < state.migrations.length; i++) {\n if (state.recoverFn !== undefined && state.recoverAtIndex === i) {\n chain = chain.recover(state.recoverFn);\n }\n chain = chain.migrate(state.migrations[i].toVersion, state.migrations[i].fn);\n }\n\n // If recover was placed at or after the last migration\n if (state.recoverFn !== undefined && state.recoverAtIndex === state.migrations.length) {\n chain = chain.recover(state.recoverFn);\n }\n\n const dataModel: DataModel<Data> = chain.init(() => configInitFn());\n\n return PluginDataModel[FROM_BUILDER]<Data, Versions, Config>(dataModel, configInitFn);\n}\n\n/**\n * A named plugin instance created by `factory.create({ pluginId, ... })`.\n * Passed to both `.transfer()` (on migration chain) and `.plugin()` (on BlockModelV3).\n * Implements TransferTarget so the migration chain can accept it.\n *\n * @typeParam Id - Plugin instance ID literal type\n * @typeParam Data - Plugin data type\n * @typeParam Params - Plugin params type\n * @typeParam Outputs - Plugin outputs type\n * @typeParam TransferData - Type of data entering the plugin via transfer (never if no transfer)\n */\nexport class PluginInstance<\n Id extends string = string,\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n TransferData = never,\n ModelServices = unknown,\n UiServices = unknown,\n> implements TransferTarget<Id, TransferData> {\n readonly id: Id;\n readonly transferVersion: string;\n /** @internal Phantom for type inference; never set at runtime. */\n readonly __instanceTypes?: {\n data: Data;\n params: Params;\n outputs: Outputs;\n modelServices: ModelServices;\n uiServices: UiServices;\n };\n /** Bound closure that creates the PluginModel. Config is captured at factory.create() time. */\n private readonly createPluginModel: () => PluginModel<\n Data,\n Params,\n Outputs,\n ModelServices,\n UiServices\n >;\n\n private constructor(\n id: Id,\n createPluginModel: () => PluginModel<Data, Params, Outputs, ModelServices, UiServices>,\n transferVersion: string,\n ) {\n this.id = id;\n this.createPluginModel = createPluginModel;\n this.transferVersion = transferVersion;\n }\n\n /** @internal Accepts concrete Config — binds it into a closure, avoiding Config variance issues. */\n static [FROM_BUILDER]<\n Id extends string,\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n TransferData,\n Config extends PluginConfig = PluginConfig,\n ModelServices = unknown,\n UiServices = unknown,\n >(\n id: Id,\n factory: PluginModelFactory<\n Data,\n Params,\n Outputs,\n Config,\n Record<string, unknown>,\n ModelServices,\n UiServices\n >,\n transferVersion: string,\n config?: Config,\n ): PluginInstance<Id, Data, Params, Outputs, TransferData, ModelServices, UiServices> {\n return new PluginInstance(id, () => factory[CREATE_PLUGIN_MODEL](config), transferVersion);\n }\n\n /** @internal Create a PluginModel from this instance. Used by BlockModelV3.plugin(). */\n [CREATE_PLUGIN_MODEL](): PluginModel<Data, Params, Outputs, ModelServices, UiServices> {\n return this.createPluginModel();\n }\n}\n\n/**\n * Configured plugin instance returned by PluginModelFactory[CREATE_PLUGIN_MODEL]().\n * Contains the plugin's name, data model, and output definitions.\n */\nexport class PluginModel<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n ModelServices = {},\n UiServices = {},\n> {\n /** Globally unique plugin name */\n readonly name: PluginName;\n /** Data model instance for this plugin */\n readonly dataModel: DataModel<Data>;\n /** Output definitions - functions that compute outputs from plugin context */\n readonly outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n /** Per-output flags (e.g. withStatus) */\n readonly outputFlags: Record<string, { withStatus: boolean }>;\n /** Feature flags declared by this plugin */\n readonly featureFlags?: BlockCodeKnownFeatureFlags;\n /** Create fresh default data. Config (if any) is captured at creation time. */\n readonly getDefaultData: () => DataVersioned<Data>;\n\n private constructor(options: {\n name: PluginName;\n dataModel: DataModel<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n outputFlags: Record<string, { withStatus: boolean }>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n getDefaultData: () => DataVersioned<Data>;\n }) {\n this.name = options.name;\n this.dataModel = options.dataModel;\n this.outputs = options.outputs;\n this.outputFlags = options.outputFlags;\n this.featureFlags = options.featureFlags;\n this.getDefaultData = options.getDefaultData;\n }\n\n /**\n * Internal method for creating PluginModel from factory.\n * Uses Symbol key to prevent external access.\n * @internal\n */\n static [FROM_BUILDER]<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataModel: DataModel<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n outputFlags: Record<string, { withStatus: boolean }>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n getDefaultData: () => DataVersioned<Data>;\n }): PluginModel<Data, Params, Outputs, ModelServices, UiServices> {\n return new PluginModel<Data, Params, Outputs, ModelServices, UiServices>(options);\n }\n\n /**\n * Creates a new PluginModelBuilder with a PluginDataModel (supports transfer / config).\n *\n * @example\n * const pluginData = new PluginDataModelBuilder().from<TableData>(\"v1\")\n * .migrate<FilteredTableData>(\"v2\", (v1) => ({ ...v1, filters: [] }))\n * .init<TableConfig>((config?) => ({\n * state: createDefaultState(config?.ops),\n * filters: [],\n * }));\n *\n * const myPlugin = PluginModel.define({\n * name: 'myPlugin' as PluginName,\n * data: pluginData,\n * }).build();\n */\n static define<\n Data extends PluginData,\n Params extends PluginParams = undefined,\n Versions extends Record<string, unknown> = {},\n Config extends PluginConfig = undefined,\n Flags extends BlockCodeKnownFeatureFlags = {},\n >(options: {\n name: PluginName;\n data: PluginDataModel<Data, Versions, Config>;\n featureFlags?: Flags;\n }): PluginModelInitialBuilder<\n Data,\n Params,\n Config,\n Versions,\n ResolveModelServices<Flags>,\n ResolveUiServices<Flags>\n >;\n /**\n * Creates a new PluginModelBuilder with a data model factory function (backward compatible).\n *\n * @example\n * const myPlugin = PluginModel.define({\n * name: 'myPlugin' as PluginName,\n * data: (cfg) => dataModelChain.init(() => ({ value: cfg.defaultValue })),\n * }).build();\n */\n static define<\n Data extends PluginData,\n Params extends PluginParams = undefined,\n Config extends PluginConfig = undefined,\n Flags extends BlockCodeKnownFeatureFlags = {},\n >(options: {\n name: PluginName;\n data: (config?: Config) => DataModel<Data>;\n featureFlags?: Flags;\n }): PluginModelInitialBuilder<\n Data,\n Params,\n Config,\n {},\n ResolveModelServices<Flags>,\n ResolveUiServices<Flags>\n >;\n static define(options: {\n name: PluginName;\n data: PluginDataModel<any, any, any> | ((config?: any) => DataModel<any>);\n featureFlags?: BlockCodeKnownFeatureFlags;\n }): PluginModelInitialBuilder {\n if (options.data instanceof PluginDataModel) {\n const pdm = options.data;\n return PluginModelInitialBuilder.create({\n name: options.name,\n dataFn: () => pdm.dataModel,\n getDefaultDataFn: (config: any) => pdm.getDefaultData(config),\n featureFlags: options.featureFlags,\n });\n }\n return PluginModelInitialBuilder.create({\n name: options.name,\n dataFn: options.data,\n featureFlags: options.featureFlags,\n });\n }\n}\n\n/** Plugin factory returned by PluginModelBuilder.build(). */\nexport interface PluginFactory<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> extends PluginFactoryLike<Data, Params, Outputs, ModelServices, UiServices> {\n /** Create a named plugin instance, optionally with transfer at a specific version. */\n create<const Id extends string, const V extends string & keyof Versions = never>(options: {\n pluginId: Id;\n transferAt?: V;\n config?: Config;\n }): PluginInstance<Id, Data, Params, Outputs, Versions[V], ModelServices, UiServices>;\n\n /**\n * @internal Phantom field for structural type extraction.\n * Enables InferFactoryData/InferFactoryOutputs to work via PluginFactoryLike.\n */\n readonly __types?: {\n data: Data;\n params: Params;\n outputs: Outputs;\n modelServices: ModelServices;\n uiServices: UiServices;\n config: Config;\n versions: Versions;\n };\n}\n\nclass PluginModelFactory<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> implements PluginFactory<Data, Params, Outputs, Config, Versions, ModelServices, UiServices> {\n private readonly name: PluginName;\n private readonly dataFn: (config?: Config) => DataModel<Data>;\n private readonly getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n readonly outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n private readonly outputFlags: Record<string, { withStatus: boolean }>;\n private readonly featureFlags?: BlockCodeKnownFeatureFlags;\n\n private constructor(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n outputFlags: Record<string, { withStatus: boolean }>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n }) {\n this.name = options.name;\n this.dataFn = options.dataFn;\n this.getDefaultDataFn = options.getDefaultDataFn;\n this.outputs = options.outputs;\n this.outputFlags = options.outputFlags;\n this.featureFlags = options.featureFlags;\n }\n\n /** @internal */\n static [FROM_BUILDER]<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n Config extends PluginConfig,\n Versions extends Record<string, unknown>,\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n outputFlags: Record<string, { withStatus: boolean }>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n }): PluginModelFactory<Data, Params, Outputs, Config, Versions, ModelServices, UiServices> {\n return new PluginModelFactory(options);\n }\n\n create<const Id extends string, const V extends string & keyof Versions = never>(options: {\n pluginId: Id;\n transferAt?: V;\n config?: Config;\n }): PluginInstance<Id, Data, Params, Outputs, Versions[V], ModelServices, UiServices> {\n const transferVersion = options.transferAt ?? NO_TRANSFER_VERSION;\n return PluginInstance[FROM_BUILDER]<\n Id,\n Data,\n Params,\n Outputs,\n Versions[V],\n Config,\n ModelServices,\n UiServices\n >(options.pluginId as Id, this, transferVersion, options.config);\n }\n\n /** @internal Create a PluginModel from config. Config is captured in getDefaultData closure. */\n [CREATE_PLUGIN_MODEL](\n config?: Config,\n ): PluginModel<Data, Params, Outputs, ModelServices, UiServices> {\n const dataModel = this.dataFn(config);\n const getDefaultDataFn = this.getDefaultDataFn;\n return PluginModel[FROM_BUILDER]<Data, Params, Outputs, ModelServices, UiServices>({\n name: this.name,\n dataModel,\n outputs: this.outputs,\n outputFlags: this.outputFlags,\n featureFlags: this.featureFlags,\n getDefaultData: getDefaultDataFn\n ? () => getDefaultDataFn(config)\n : () => dataModel.getDefaultData(),\n });\n }\n}\n\n/**\n * Builder for creating PluginType with type-safe output definitions.\n *\n * Use `PluginModel.define()` to create a builder instance.\n *\n * @typeParam Data - Plugin's persistent data type\n * @typeParam Params - Params derived from block's RenderCtx (optional)\n * @typeParam Config - Static configuration passed to plugin factory (optional)\n * @typeParam Outputs - Accumulated output types\n * @typeParam Versions - Version map from PluginDataModel (empty for function-based data)\n *\n * @example\n * const dataTable = PluginModel.define({\n * name: 'dataTable' as PluginName,\n * data: pluginDataModel,\n * })\n * .output('model', (ctx) => createTableModel(ctx))\n * .build();\n */\nclass PluginModelBuilder<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> {\n protected readonly name: PluginName;\n protected readonly dataFn: (config?: Config) => DataModel<Data>;\n protected readonly getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n private readonly outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n private readonly outputFlags: Record<string, { withStatus: boolean }>;\n protected readonly featureFlags?: BlockCodeKnownFeatureFlags;\n\n protected constructor(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs?: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n outputFlags?: Record<string, { withStatus: boolean }>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n }) {\n this.name = options.name;\n this.dataFn = options.dataFn;\n this.getDefaultDataFn = options.getDefaultDataFn;\n this.outputs =\n options.outputs ?? ({} as PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>);\n this.outputFlags = options.outputFlags ?? {};\n this.featureFlags = options.featureFlags;\n }\n\n /** @internal */\n static [FROM_BUILDER]<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n Config extends PluginConfig,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs?: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n outputFlags?: Record<string, { withStatus: boolean }>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n }): PluginModelBuilder<Data, Params, Outputs, Config, Versions, ModelServices, UiServices> {\n return new PluginModelBuilder(options);\n }\n\n /**\n * Adds an output to the plugin.\n *\n * @param key - Output name\n * @param fn - Function that computes the output value from plugin context\n * @returns PluginModel with the new output added\n *\n * @example\n * .output('model', (ctx) => createModel(ctx.params.columns, ctx.data.state))\n * .output('isReady', (ctx) => ctx.params.columns !== undefined)\n */\n output<const Key extends string, T>(\n key: Key,\n fn: (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n ) => T,\n ): PluginModelBuilder<\n Data,\n Params,\n Outputs & { [K in Key]: T },\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return new PluginModelBuilder<\n Data,\n Params,\n Outputs & { [K in Key]: T },\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n featureFlags: this.featureFlags,\n outputs: {\n ...this.outputs,\n [key]: fn,\n } as {\n [K in keyof (Outputs & { [P in Key]: T })]: (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n ) => (Outputs & { [P in Key]: T })[K];\n },\n outputFlags: { ...this.outputFlags, [key]: { withStatus: false } },\n });\n }\n\n /**\n * Adds an output wrapped with status information to the plugin.\n *\n * The UI receives the full {@link OutputWithStatus} object instead of an unwrapped value,\n * allowing it to distinguish between pending, success, and error states.\n *\n * @param key - Output name\n * @param fn - Function that computes the output value from plugin context\n * @returns PluginModel with the new status-wrapped output added\n *\n * @example\n * .outputWithStatus('table', (ctx) => {\n * const pCols = ctx.params.pFrame?.getPColumns();\n * if (pCols === undefined) return undefined;\n * return createPlDataTableV2(ctx, pCols, ctx.data.tableState);\n * })\n */\n outputWithStatus<const Key extends string, T>(\n key: Key,\n fn: (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n ) => T,\n ): PluginModelBuilder<\n Data,\n Params,\n Outputs & { [K in Key]: OutputWithStatus<T> },\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return new PluginModelBuilder<\n Data,\n Params,\n Outputs & { [K in Key]: OutputWithStatus<T> },\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n featureFlags: this.featureFlags,\n outputs: {\n ...this.outputs,\n [key]: fn,\n } as {\n [K in keyof (Outputs & { [P in Key]: OutputWithStatus<T> })]: (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n ) => (Outputs & { [P in Key]: OutputWithStatus<T> })[K];\n },\n outputFlags: { ...this.outputFlags, [key]: { withStatus: true } },\n });\n }\n\n /**\n * Finalizes the plugin definition and returns a PluginFactory.\n *\n * @returns Plugin factory that creates named plugin instances via .create()\n *\n * @example\n * const myPlugin = PluginModel.define({ ... })\n * .output('value', (ctx) => ctx.data.value)\n * .build();\n *\n * // Create a named instance:\n * const table = myPlugin.create({ pluginId: 'mainTable', config: { ... } });\n */\n build(): PluginFactory<Data, Params, Outputs, Config, Versions, ModelServices, UiServices> {\n return PluginModelFactory[FROM_BUILDER]<\n Data,\n Params,\n Outputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n outputs: this.outputs,\n outputFlags: this.outputFlags,\n featureFlags: this.featureFlags,\n });\n }\n}\n\n/**\n * Initial builder returned by PluginModel.define(). Extends PluginModelBuilder with .params().\n * Once .params() or .output() is called, transitions to PluginModelBuilder (no second .params()).\n */\nclass PluginModelInitialBuilder<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> extends PluginModelBuilder<Data, Params, {}, Config, Versions, ModelServices, UiServices> {\n /** @internal */\n static create<\n Data extends PluginData,\n Config extends PluginConfig,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n }): PluginModelInitialBuilder<Data, undefined, Config, Versions, ModelServices, UiServices> {\n return new PluginModelInitialBuilder(options);\n }\n\n /**\n * Sets the Params type for this plugin — the shape of data derived from the block's\n * render context and passed into plugin output functions via `ctx.params`.\n * Must be called before .output(). Available only on the initial builder.\n *\n * @example\n * .params<{ title: string }>()\n * .output('displayText', (ctx) => ctx.params.title)\n */\n params<P extends PluginParams>(): PluginModelBuilder<\n Data,\n P,\n {},\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return PluginModelBuilder[FROM_BUILDER]<\n Data,\n P,\n {},\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n featureFlags: this.featureFlags,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin_model.js","names":[],"sources":["../src/plugin_model.ts"],"sourcesContent":["/**\n * PluginModel - Builder for creating plugin types with data model and outputs.\n *\n * Plugins are UI components with their own model logic and persistent state.\n * Block developers register plugin instances via BlockModelV3.plugin() method.\n *\n * @module plugin_model\n */\n\nimport type { BlockCodeKnownFeatureFlags, OutputWithStatus } from \"@milaboratories/pl-model-common\";\nimport type { ResolveModelServices, ResolveUiServices } from \"./services/service_resolve\";\nimport {\n type DataModel,\n DataModelBuilder,\n type DataRecoverFn,\n type DataVersioned,\n type TransferTarget,\n} from \"./block_migrations\";\nimport { type PluginName, DATA_MODEL_LEGACY_VERSION } from \"./block_storage\";\nimport type { PluginFactoryLike } from \"./plugin_handle\";\nimport type { PluginRenderCtx } from \"./render\";\n\n/** Symbol for internal builder creation method */\nconst FROM_BUILDER = Symbol(\"fromBuilder\");\n\n/** Output function signature for plugin render context. */\ntype PluginOutputFn<\n Data extends PluginData,\n Params extends PluginParams,\n ModelServices,\n UiServices,\n T,\n> = (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n) => T;\n\n/** Mapped output functions for a plugin's outputs. */\ntype PluginOutputFns<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n ModelServices,\n UiServices,\n> = {\n [K in keyof Outputs]: PluginOutputFn<Data, Params, ModelServices, UiServices, Outputs[K]>;\n};\n\n/** Symbol for internal plugin model creation — not accessible to external consumers */\nexport const CREATE_PLUGIN_MODEL = Symbol(\"createPluginModel\");\n\n/** Sentinel for PluginInstance without transferAt — no transfer possible. */\nconst NO_TRANSFER_VERSION = \"\";\n\n/**\n * Runtime definition for a single public output field.\n * Stored in PluginModel and passed through BlockModelInfo to the UI layer.\n */\nexport type PublicOutputFieldDef = {\n readonly getter: (data: unknown) => unknown;\n};\n\nexport type PublicOutputDef<PublicOutputs extends PluginPublicOutputs> = {\n [K in keyof PublicOutputs]: PublicOutputFieldDef;\n};\n\nexport type PluginData = Record<string, unknown>;\nexport type PluginParams = undefined | Record<string, unknown>;\nexport type PluginOutputs = Record<string, unknown>;\nexport type PluginPublicOutputs = Record<string, unknown>;\nexport type PluginConfig = undefined | Record<string, unknown>;\n\n/**\n * Plugin data model with typed migration chain and config-aware initialization.\n *\n * @typeParam Data - Current (latest) plugin data type\n * @typeParam Versions - Map of version keys to their data types (accumulated by the chain)\n * @typeParam Config - Config type passed to init function (undefined if none)\n */\nexport class PluginDataModel<\n Data extends PluginData,\n Versions extends Record<string, unknown> = {},\n Config = undefined,\n> {\n readonly dataModel: DataModel<Data>;\n private readonly configInitFn: (config?: Config) => Data;\n\n /** @internal Phantom field to anchor the Versions type parameter. */\n declare readonly __versions?: Versions;\n\n private constructor(dataModel: DataModel<Data>, configInitFn: (config?: Config) => Data) {\n this.dataModel = dataModel;\n this.configInitFn = configInitFn;\n }\n\n /** @internal */\n static [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>, Config>(\n dataModel: DataModel<Data>,\n configInitFn: (config?: Config) => Data,\n ): PluginDataModel<Data, Versions, Config> {\n return new PluginDataModel<Data, Versions, Config>(dataModel, configInitFn);\n }\n\n /** Create fresh data with optional config. */\n getDefaultData(config?: Config): DataVersioned<Data> {\n const data = this.configInitFn(config);\n return { version: this.dataModel.version, data };\n }\n}\n\n/** Internal state for plugin data model chain. */\ntype PluginChainState = {\n initialVersion: string;\n migrations: Array<{ toVersion: string; fn: (data: unknown) => unknown }>;\n recoverFn?: (version: string, data: unknown) => unknown;\n recoverAtIndex?: number;\n};\n\n/** Version → persisted data shape for each migration step (third generic of `PluginModel.define` when `data` is a `PluginDataModel`). */\nexport type PluginDataModelVersions<\n M extends PluginDataModel<PluginData, Record<string, unknown>, unknown>,\n> = M extends PluginDataModel<PluginData, infer Versions, unknown> ? Versions : never;\n\n/**\n * Builder for creating PluginDataModel with type-safe migrations.\n * Mirrors DataModelBuilder — same .from(), .migrate(), .recover(), .init() chain.\n *\n * @example\n * const pluginData = new PluginDataModelBuilder()\n * .from<TableData>(\"v1\")\n * .migrate<FilteredTableData>(\"v2\", (v1) => ({ ...v1, filters: [] }))\n * .init<TableConfig>((config?) => ({\n * state: createDefaultState(config?.ops),\n * filters: [],\n * }));\n */\nexport class PluginDataModelBuilder {\n from<Data extends PluginData, const V extends string>(\n version: V,\n ): PluginDataModelInitialChain<Data, Record<V, Data>> {\n return PluginDataModelInitialChain[FROM_BUILDER]<Data, Record<V, Data>>({\n initialVersion: version,\n migrations: [],\n });\n }\n}\n\n/**\n * Chain returned by .migrate(). Supports .migrate(), .recover(), .init().\n * No .upgradeLegacy() — that is only available on the initial chain.\n */\nexport class PluginDataModelChain<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n> {\n protected constructor(protected readonly state: PluginChainState) {}\n\n /** @internal */\n static [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>>(\n state: PluginChainState,\n ): PluginDataModelChain<Data, Versions> {\n return new PluginDataModelChain(state);\n }\n\n /**\n * Add a migration step transforming data from the current version to the next.\n */\n migrate<Next extends PluginData, const NextV extends string>(\n version: NextV,\n fn: (current: Data) => Next,\n ): PluginDataModelChain<Next, Versions & Record<NextV, Next>> {\n return PluginDataModelChain[FROM_BUILDER]<Next, Versions & Record<NextV, Next>>({\n ...this.state,\n migrations: [\n ...this.state.migrations,\n { toVersion: version, fn: fn as (data: unknown) => unknown },\n ],\n });\n }\n\n /**\n * Set a recovery handler for unknown or legacy versions.\n * Can only be called once — the returned chain has no recover() method.\n */\n recover(fn: DataRecoverFn<Data>): PluginDataModelWithRecover<Data, Versions> {\n return PluginDataModelWithRecover[FROM_BUILDER]<Data, Versions>({\n ...this.state,\n recoverFn: fn as (version: string, data: unknown) => unknown,\n recoverAtIndex: this.state.migrations.length,\n });\n }\n\n /** Finalize the PluginDataModel. */\n init<Config = undefined>(fn: (config?: Config) => Data): PluginDataModel<Data, Versions, Config> {\n return buildPluginDataModel(this.state, fn);\n }\n}\n\n/**\n * Initial chain returned by new PluginDataModelBuilder().from().\n * Extends PluginDataModelChain with .upgradeLegacy() — available only before\n * any .migrate() calls, matching the block's DataModelInitialChain pattern.\n */\nexport class PluginDataModelInitialChain<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n> extends PluginDataModelChain<Data, Versions> {\n /** @internal */\n static override [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>>(\n state: PluginChainState,\n ): PluginDataModelInitialChain<Data, Versions> {\n return new PluginDataModelInitialChain(state);\n }\n\n /**\n * Handle data from a previous plugin type occupying this slot.\n * Prepends a migration from DATA_MODEL_LEGACY_VERSION to the initial version.\n * When a plugin type changes, the old data arrives with DATA_MODEL_LEGACY_VERSION —\n * this function transforms it into the initial version, then the normal chain runs.\n *\n * Must be called right after .from() — not available after .migrate().\n * Mutually exclusive with recover().\n *\n * @param fn - Transform from old plugin's raw data to this plugin's initial data type\n */\n upgradeLegacy(fn: (data: unknown) => Data): PluginDataModelWithRecover<Data, Versions> {\n return PluginDataModelWithRecover[FROM_BUILDER]<Data, Versions>({\n ...this.state,\n migrations: [\n { toVersion: this.state.initialVersion, fn: fn as (data: unknown) => unknown },\n ...this.state.migrations,\n ],\n initialVersion: DATA_MODEL_LEGACY_VERSION,\n });\n }\n}\n\n/**\n * Chain after .recover() — supports .migrate(), .init(). No second recover().\n */\nexport class PluginDataModelWithRecover<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n> {\n private constructor(private readonly state: PluginChainState) {}\n\n /** @internal */\n static [FROM_BUILDER]<Data extends PluginData, Versions extends Record<string, unknown>>(\n state: PluginChainState,\n ): PluginDataModelWithRecover<Data, Versions> {\n return new PluginDataModelWithRecover(state);\n }\n\n migrate<Next extends PluginData, const NextV extends string>(\n version: NextV,\n fn: (current: Data) => Next,\n ): PluginDataModelWithRecover<Next, Versions & Record<NextV, Next>> {\n return PluginDataModelWithRecover[FROM_BUILDER]<Next, Versions & Record<NextV, Next>>({\n ...this.state,\n migrations: [\n ...this.state.migrations,\n { toVersion: version, fn: fn as (data: unknown) => unknown },\n ],\n });\n }\n\n init<Config = undefined>(fn: (config?: Config) => Data): PluginDataModel<Data, Versions, Config> {\n return buildPluginDataModel(this.state, fn);\n }\n}\n\n/**\n * Builds a PluginDataModel by replaying the stored chain state through DataModelBuilder.\n * @internal\n */\nfunction buildPluginDataModel<\n Data extends PluginData,\n Versions extends Record<string, unknown>,\n Config,\n>(\n state: PluginChainState,\n configInitFn: (config?: Config) => Data,\n): PluginDataModel<Data, Versions, Config> {\n // Build inner DataModel by replaying migrations through DataModelBuilder chain.\n // Uses `any` internally — type safety is maintained by the public chain types.\n let chain: any = new DataModelBuilder().from(state.initialVersion);\n\n for (let i = 0; i < state.migrations.length; i++) {\n if (state.recoverFn !== undefined && state.recoverAtIndex === i) {\n chain = chain.recover(state.recoverFn);\n }\n chain = chain.migrate(state.migrations[i].toVersion, state.migrations[i].fn);\n }\n\n // If recover was placed at or after the last migration\n if (state.recoverFn !== undefined && state.recoverAtIndex === state.migrations.length) {\n chain = chain.recover(state.recoverFn);\n }\n\n const dataModel: DataModel<Data> = chain.init(() => configInitFn());\n\n return PluginDataModel[FROM_BUILDER]<Data, Versions, Config>(dataModel, configInitFn);\n}\n\n/**\n * A named plugin instance created by `factory.create({ pluginId, ... })`.\n * Passed to both `.transfer()` (on migration chain) and `.plugin()` (on BlockModelV3).\n * Implements TransferTarget so the migration chain can accept it.\n *\n * @typeParam Id - Plugin instance ID literal type\n * @typeParam Data - Plugin data type\n * @typeParam Params - Plugin params type\n * @typeParam Outputs - Plugin outputs type\n * @typeParam TransferData - Type of data entering the plugin via transfer (never if no transfer)\n */\nexport class PluginInstance<\n Id extends string = string,\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n TransferData = never,\n ModelServices = unknown,\n UiServices = unknown,\n> implements TransferTarget<Id, TransferData> {\n readonly id: Id;\n readonly transferVersion: string;\n /** @internal Phantom for type inference; never set at runtime. */\n readonly __instanceTypes?: {\n data: Data;\n params: Params;\n outputs: Outputs;\n modelServices: ModelServices;\n uiServices: UiServices;\n publicOutputs: PublicOutputs;\n };\n /** Bound closure that creates the PluginModel. Config is captured at factory.create() time. */\n private readonly createPluginModel: () => PluginModel<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n ModelServices,\n UiServices\n >;\n\n private constructor(\n id: Id,\n createPluginModel: () => PluginModel<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n ModelServices,\n UiServices\n >,\n transferVersion: string,\n ) {\n this.id = id;\n this.createPluginModel = createPluginModel;\n this.transferVersion = transferVersion;\n }\n\n /** @internal Accepts concrete Config — binds it into a closure, avoiding Config variance issues. */\n static [FROM_BUILDER]<\n Id extends string,\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n TransferData = never,\n Config extends PluginConfig = PluginConfig,\n ModelServices = unknown,\n UiServices = unknown,\n >(\n id: Id,\n factory: PluginModelFactory<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Config,\n Record<string, unknown>,\n ModelServices,\n UiServices\n >,\n transferVersion: string,\n config?: Config,\n ): PluginInstance<\n Id,\n Data,\n Params,\n Outputs,\n PublicOutputs,\n TransferData,\n ModelServices,\n UiServices\n > {\n return new PluginInstance(id, () => factory[CREATE_PLUGIN_MODEL](config), transferVersion);\n }\n\n /** @internal Create a PluginModel from this instance. Used by BlockModelV3.plugin(). */\n [CREATE_PLUGIN_MODEL](): PluginModel<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n ModelServices,\n UiServices\n > {\n return this.createPluginModel();\n }\n}\n\n/**\n * Configured plugin instance returned by PluginModelFactory[CREATE_PLUGIN_MODEL]().\n * Contains the plugin's name, data model, and output definitions.\n */\nexport class PluginModel<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n ModelServices = {},\n UiServices = {},\n> {\n /** Globally unique plugin name */\n readonly name: PluginName;\n /** Data model instance for this plugin */\n readonly dataModel: DataModel<Data>;\n /** Output definitions - functions that compute outputs from plugin context */\n readonly outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n /** Feature flags declared by this plugin */\n readonly featureFlags?: BlockCodeKnownFeatureFlags;\n /** Create fresh default data. Config (if any) is captured at creation time. */\n readonly getDefaultData: () => DataVersioned<Data>;\n /** Public output definitions — accessible without usePlugin() via app.plugins */\n readonly publicOutputDef: PublicOutputDef<PublicOutputs>;\n\n private constructor(options: {\n name: PluginName;\n dataModel: DataModel<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n getDefaultData: () => DataVersioned<Data>;\n publicOutputDef: PublicOutputDef<PublicOutputs>;\n }) {\n this.name = options.name;\n this.dataModel = options.dataModel;\n this.outputs = options.outputs;\n this.featureFlags = options.featureFlags;\n this.getDefaultData = options.getDefaultData;\n this.publicOutputDef = options.publicOutputDef;\n }\n\n /**\n * Internal method for creating PluginModel from factory.\n * Uses Symbol key to prevent external access.\n * @internal\n */\n static [FROM_BUILDER]<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataModel: DataModel<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n getDefaultData: () => DataVersioned<Data>;\n publicOutputDef: PublicOutputDef<PublicOutputs>;\n }): PluginModel<Data, Params, Outputs, PublicOutputs, ModelServices, UiServices> {\n return new PluginModel<Data, Params, Outputs, PublicOutputs, ModelServices, UiServices>(\n options,\n );\n }\n\n /**\n * Creates a new PluginModelBuilder with a PluginDataModel (supports transfer / config).\n *\n * @example\n * const pluginData = new PluginDataModelBuilder().from<TableData>(\"v1\")\n * .migrate<FilteredTableData>(\"v2\", (v1) => ({ ...v1, filters: [] }))\n * .init<TableConfig>((config?) => ({\n * state: createDefaultState(config?.ops),\n * filters: [],\n * }));\n *\n * const myPlugin = PluginModel.define({\n * name: 'myPlugin' as PluginName,\n * data: pluginData,\n * }).build();\n */\n static define<\n Data extends PluginData,\n Params extends PluginParams = undefined,\n Versions extends Record<string, unknown> = {},\n Config extends PluginConfig = undefined,\n Flags extends BlockCodeKnownFeatureFlags = {},\n >(options: {\n name: PluginName;\n data: PluginDataModel<Data, Versions, Config>;\n featureFlags?: Flags;\n }): PluginModelInitialBuilder<\n Data,\n Params,\n Config,\n Versions,\n ResolveModelServices<Flags>,\n ResolveUiServices<Flags>\n >;\n /**\n * Creates a new PluginModelBuilder with a data model factory function (backward compatible).\n *\n * @example\n * const myPlugin = PluginModel.define({\n * name: 'myPlugin' as PluginName,\n * data: (cfg) => dataModelChain.init(() => ({ value: cfg.defaultValue })),\n * }).build();\n */\n static define<\n Data extends PluginData,\n Params extends PluginParams = undefined,\n Config extends PluginConfig = undefined,\n Flags extends BlockCodeKnownFeatureFlags = {},\n >(options: {\n name: PluginName;\n data: (config?: Config) => DataModel<Data>;\n featureFlags?: Flags;\n }): PluginModelInitialBuilder<\n Data,\n Params,\n Config,\n {},\n ResolveModelServices<Flags>,\n ResolveUiServices<Flags>\n >;\n static define(options: {\n name: PluginName;\n data: PluginDataModel<any, any, any> | ((config?: any) => DataModel<any>);\n featureFlags?: BlockCodeKnownFeatureFlags;\n }): PluginModelInitialBuilder {\n if (options.data instanceof PluginDataModel) {\n const pdm = options.data;\n return PluginModelInitialBuilder.create({\n name: options.name,\n dataFn: () => pdm.dataModel,\n getDefaultDataFn: (config: any) => pdm.getDefaultData(config),\n featureFlags: options.featureFlags,\n });\n }\n return PluginModelInitialBuilder.create({\n name: options.name,\n dataFn: options.data,\n featureFlags: options.featureFlags,\n });\n }\n}\n\n/** Plugin factory returned by PluginModelBuilder.build(). */\nexport interface PluginFactory<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> extends PluginFactoryLike<Data, Params, Outputs, ModelServices, UiServices> {\n /** Create a named plugin instance, optionally with transfer at a specific version. */\n create<const Id extends string, const V extends string & keyof Versions = never>(options: {\n pluginId: Id;\n transferAt?: V;\n config?: Config;\n }): PluginInstance<\n Id,\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Versions[V],\n ModelServices,\n UiServices\n >;\n\n /** Public output field definitions declared via .publicOutput(). */\n readonly publicOutputDef: PublicOutputDef<PublicOutputs>;\n\n /**\n * @internal Phantom field for structural type extraction.\n * Enables InferFactoryData/InferFactoryOutputs to work via PluginFactoryLike.\n */\n readonly __types?: {\n data: Data;\n params: Params;\n outputs: Outputs;\n modelServices: ModelServices;\n uiServices: UiServices;\n config: Config;\n versions: Versions;\n publicOutputs: PublicOutputs;\n };\n}\n\nclass PluginModelFactory<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> implements PluginFactory<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n> {\n private readonly name: PluginName;\n private readonly dataFn: (config?: Config) => DataModel<Data>;\n private readonly getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n readonly outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n private readonly featureFlags?: BlockCodeKnownFeatureFlags;\n readonly publicOutputDef: PublicOutputDef<PublicOutputs>;\n\n private constructor(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n publicOutputDef: PublicOutputDef<PublicOutputs>;\n }) {\n this.name = options.name;\n this.dataFn = options.dataFn;\n this.getDefaultDataFn = options.getDefaultDataFn;\n this.outputs = options.outputs;\n this.featureFlags = options.featureFlags;\n this.publicOutputDef = options.publicOutputDef;\n }\n\n /** @internal */\n static [FROM_BUILDER]<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n publicOutputDef: PublicOutputDef<PublicOutputs>;\n }): PluginModelFactory<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return new PluginModelFactory(options);\n }\n\n create<const Id extends string, const V extends string & keyof Versions = never>(options: {\n pluginId: Id;\n transferAt?: V;\n config?: Config;\n }): PluginInstance<\n Id,\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Versions[V],\n ModelServices,\n UiServices\n > {\n const transferVersion = options.transferAt ?? NO_TRANSFER_VERSION;\n return PluginInstance[FROM_BUILDER]<\n Id,\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Versions[V],\n Config,\n ModelServices,\n UiServices\n >(options.pluginId as Id, this, transferVersion, options.config);\n }\n\n /** @internal Create a PluginModel from config. Config is captured in getDefaultData closure. */\n [CREATE_PLUGIN_MODEL](\n config?: Config,\n ): PluginModel<Data, Params, Outputs, PublicOutputs, ModelServices, UiServices> {\n const dataModel = this.dataFn(config);\n const getDefaultDataFn = this.getDefaultDataFn;\n return PluginModel[FROM_BUILDER]<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataModel,\n outputs: this.outputs,\n featureFlags: this.featureFlags,\n getDefaultData: getDefaultDataFn\n ? () => getDefaultDataFn(config)\n : () => dataModel.getDefaultData(),\n publicOutputDef: this.publicOutputDef,\n });\n }\n}\n\n/**\n * Builder for creating PluginType with type-safe output definitions.\n *\n * Use `PluginModel.define()` to create a builder instance.\n *\n * @typeParam Data - Plugin's persistent data type\n * @typeParam Params - Params derived from block's RenderCtx (optional)\n * @typeParam Config - Static configuration passed to plugin factory (optional)\n * @typeParam Outputs - Accumulated output types\n * @typeParam Versions - Version map from PluginDataModel (empty for function-based data)\n *\n * @example\n * const dataTable = PluginModel.define({\n * name: 'dataTable' as PluginName,\n * data: pluginDataModel,\n * })\n * .output('model', (ctx) => createTableModel(ctx))\n * .build();\n */\nclass PluginModelBuilder<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Outputs extends PluginOutputs = PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> {\n protected readonly name: PluginName;\n protected readonly dataFn: (config?: Config) => DataModel<Data>;\n protected readonly getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n private readonly outputs: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n protected readonly featureFlags?: BlockCodeKnownFeatureFlags;\n private readonly publicOutputDef: PublicOutputDef<PublicOutputs>;\n\n protected constructor(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs?: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n publicOutputDef: PublicOutputDef<PublicOutputs>;\n }) {\n this.name = options.name;\n this.dataFn = options.dataFn;\n this.getDefaultDataFn = options.getDefaultDataFn;\n this.outputs =\n options.outputs ?? ({} as PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>);\n this.featureFlags = options.featureFlags;\n this.publicOutputDef = options.publicOutputDef;\n }\n\n /** @internal */\n static [FROM_BUILDER]<\n Data extends PluginData,\n Params extends PluginParams,\n Outputs extends PluginOutputs,\n PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n outputs?: PluginOutputFns<Data, Params, Outputs, ModelServices, UiServices>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n publicOutputDef: PublicOutputDef<PublicOutputs>;\n }): PluginModelBuilder<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return new PluginModelBuilder(options);\n }\n\n /**\n * Adds an output to the plugin.\n * All plugin outputs are always wrapped with status — the UI receives\n * {@link OutputWithStatus} for every output, allowing it to distinguish\n * between pending, success, and error states.\n *\n * @param key - Output name\n * @param fn - Function that computes the output value from plugin context\n * @returns Builder with the new output added\n *\n * @example\n * .output('model', (ctx) => createModel(ctx.params.columns, ctx.data.state))\n * .output('pFrame', (ctx) => ctx.createPFrame([...]))\n */\n output<const Key extends string, T>(\n key: Key,\n fn: (\n ctx: PluginRenderCtx<\n PluginFactoryLike<Data, Params, Record<string, unknown>, ModelServices, UiServices>\n >,\n ) => T,\n ): PluginModelBuilder<\n Data,\n Params,\n Outputs & { [K in Key]: OutputWithStatus<T> },\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return new PluginModelBuilder<\n Data,\n Params,\n Outputs & { [K in Key]: OutputWithStatus<T> },\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n featureFlags: this.featureFlags,\n outputs: {\n ...this.outputs,\n [key]: fn,\n } as unknown as PluginOutputFns<\n Data,\n Params,\n Outputs & { [P in Key]: OutputWithStatus<T> },\n ModelServices,\n UiServices\n >,\n publicOutputDef: this.publicOutputDef,\n });\n }\n\n /**\n * Exposes a plugin data field as a public output — accessible without `usePlugin()`\n * via `app.plugins.pluginName.publicOutputs.key`.\n *\n * The getter runs against the plugin's reactive data object.\n * Public outputs are read-only.\n *\n * @param key - Name of the public output field\n * @param getter - Function deriving the value from plugin data\n *\n * @example\n * .publicOutput('selection', (d) => d.selection)\n */\n publicOutput<const Key extends string, T>(\n key: Key,\n getter: (data: Data) => T,\n ): PluginModelBuilder<\n Data,\n Params,\n Outputs,\n PublicOutputs & { [K in Key]: T },\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return new PluginModelBuilder<\n Data,\n Params,\n Outputs,\n PublicOutputs & { [K in Key]: T },\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n featureFlags: this.featureFlags,\n outputs: this.outputs,\n publicOutputDef: {\n ...this.publicOutputDef,\n [key]: { getter: getter as (data: unknown) => unknown },\n } as PublicOutputDef<PublicOutputs & { [K in Key]: T }>,\n });\n }\n\n /**\n * Finalizes the plugin definition and returns a PluginFactory.\n *\n * @returns Plugin factory that creates named plugin instances via .create()\n *\n * @example\n * const myPlugin = PluginModel.define({ ... })\n * .output('value', (ctx) => ctx.data.value)\n * .build();\n *\n * // Create a named instance:\n * const table = myPlugin.create({ pluginId: 'mainTable', config: { ... } });\n */\n build(): PluginFactory<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return PluginModelFactory[FROM_BUILDER]<\n Data,\n Params,\n Outputs,\n PublicOutputs,\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n outputs: this.outputs,\n publicOutputDef: this.publicOutputDef,\n featureFlags: this.featureFlags,\n });\n }\n}\n\n/**\n * Initial builder returned by PluginModel.define(). Extends PluginModelBuilder with .params().\n * Once .params() or .output() is called, transitions to PluginModelBuilder (no second .params()).\n */\nclass PluginModelInitialBuilder<\n Data extends PluginData = PluginData,\n Params extends PluginParams = undefined,\n Config extends PluginConfig = undefined,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n> extends PluginModelBuilder<Data, Params, {}, {}, Config, Versions, ModelServices, UiServices> {\n /** @internal */\n static create<\n Data extends PluginData,\n Config extends PluginConfig,\n Versions extends Record<string, unknown> = {},\n ModelServices = {},\n UiServices = {},\n >(options: {\n name: PluginName;\n dataFn: (config?: Config) => DataModel<Data>;\n getDefaultDataFn?: (config?: Config) => DataVersioned<Data>;\n featureFlags?: BlockCodeKnownFeatureFlags;\n }): PluginModelInitialBuilder<Data, undefined, Config, Versions, ModelServices, UiServices> {\n return new PluginModelInitialBuilder({ ...options, publicOutputDef: {} });\n }\n\n /**\n * Sets the Params type for this plugin — the shape of data derived from the block's\n * render context and passed into plugin output functions via `ctx.params`.\n * Must be called before .output(). Available only on the initial builder.\n *\n * @example\n * .params<{ title: string }>()\n * .output('displayText', (ctx) => ctx.params.title)\n */\n params<P extends PluginParams>(): PluginModelBuilder<\n Data,\n P,\n {},\n {},\n Config,\n Versions,\n ModelServices,\n UiServices\n > {\n return PluginModelBuilder[FROM_BUILDER]<\n Data,\n P,\n {},\n {},\n Config,\n Versions,\n ModelServices,\n UiServices\n >({\n name: this.name,\n dataFn: this.dataFn,\n getDefaultDataFn: this.getDefaultDataFn,\n featureFlags: this.featureFlags,\n publicOutputDef: {},\n });\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/model",
3
- "version": "1.75.5",
3
+ "version": "1.75.10",
4
4
  "description": "Platforma.bio SDK / Block Model",
5
5
  "files": [
6
6
  "./dist/**/*",
@@ -30,22 +30,22 @@
30
30
  "fast-json-patch": "^3.1.1",
31
31
  "utility-types": "^3.11.0",
32
32
  "zod": "~3.25.76",
33
+ "@milaboratories/helpers": "1.14.2",
33
34
  "@milaboratories/pl-error-like": "1.12.10",
34
- "@milaboratories/pl-model-common": "1.41.2",
35
35
  "@milaboratories/pl-model-middle-layer": "1.19.3",
36
- "@milaboratories/ptabler-expression-js": "1.2.24",
37
- "@milaboratories/helpers": "1.14.2"
36
+ "@milaboratories/pl-model-common": "1.41.2",
37
+ "@milaboratories/ptabler-expression-js": "1.2.24"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@vitest/coverage-istanbul": "^4.1.3",
41
41
  "fast-json-patch": "^3.1.1",
42
42
  "typescript": "~5.9.3",
43
43
  "vitest": "^4.1.3",
44
- "@milaboratories/build-configs": "2.0.0",
45
- "@milaboratories/pf-driver": "1.4.9",
46
- "@milaboratories/pf-spec-driver": "1.3.14",
47
44
  "@milaboratories/ts-builder": "1.4.0",
48
- "@milaboratories/ts-configs": "1.2.3"
45
+ "@milaboratories/build-configs": "2.0.0",
46
+ "@milaboratories/pf-spec-driver": "1.3.15",
47
+ "@milaboratories/ts-configs": "1.2.3",
48
+ "@milaboratories/pf-driver": "1.4.10"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "ts-builder build --target node",
@@ -14,7 +14,13 @@ import type { BlockDefaultUiServices } from "./services/service_resolve";
14
14
  import { BLOCK_SERVICE_FLAGS } from "./services/block_services";
15
15
  import type { InferRenderFunctionReturn, RenderFunction } from "./render";
16
16
  import { BlockRenderCtx, PluginRenderCtx } from "./render";
17
- import type { PluginData, PluginModel, PluginOutputs, PluginParams } from "./plugin_model";
17
+ import type {
18
+ PluginData,
19
+ PluginModel,
20
+ PluginOutputs,
21
+ PluginParams,
22
+ PluginPublicOutputs,
23
+ } from "./plugin_model";
18
24
  import { PluginInstance as PluginInstanceClass, CREATE_PLUGIN_MODEL } from "./plugin_model";
19
25
  import { type PluginHandle, pluginOutputKey } from "./plugin_handle";
20
26
  import type { RenderCtxBase } from "./render";
@@ -84,16 +90,16 @@ function mergeFeatureFlags(
84
90
 
85
91
  /**
86
92
  * Plugin record: model + param derivation lambdas.
87
- * Type parameters are carried by PluginModel generic.
88
93
  */
89
94
  export type PluginRecord<
90
95
  Data extends PluginData = PluginData,
91
96
  Params extends PluginParams = undefined,
92
97
  Outputs extends PluginOutputs = PluginOutputs,
98
+ PublicOutputs extends PluginPublicOutputs = PluginPublicOutputs,
93
99
  ModelServices = unknown,
94
100
  UiServices = unknown,
95
101
  > = {
96
- readonly model: PluginModel<Data, Params, Outputs, ModelServices, UiServices>;
102
+ readonly model: PluginModel<Data, Params, Outputs, PublicOutputs, ModelServices, UiServices>;
97
103
  readonly inputs: ParamsInputErased;
98
104
  };
99
105
 
@@ -430,6 +436,7 @@ export class BlockModelV3<
430
436
  PData extends PluginData,
431
437
  PParams extends PluginParams,
432
438
  POutputs extends PluginOutputs,
439
+ PPublicOutputs extends PluginPublicOutputs,
433
440
  PTransferData,
434
441
  PluginModelServices,
435
442
  PluginUiServices,
@@ -447,6 +454,7 @@ export class BlockModelV3<
447
454
  PData,
448
455
  PParams,
449
456
  POutputs,
457
+ PPublicOutputs,
450
458
  PTransferData,
451
459
  PluginModelServices,
452
460
  PluginUiServices
@@ -462,6 +470,7 @@ export class BlockModelV3<
462
470
  PData,
463
471
  PParams,
464
472
  POutputs,
473
+ PPublicOutputs,
465
474
  PluginModelServices,
466
475
  PluginUiServices
467
476
  >;
@@ -581,13 +590,12 @@ export class BlockModelV3<
581
590
 
582
591
  // Register plugin outputs (in config pack, evaluated by middle layer)
583
592
  const outputs = model.outputs as Record<string, (ctx: PluginRenderCtx) => unknown>;
584
- const { outputFlags } = model;
585
593
  for (const [outputKey, outputFn] of Object.entries(outputs)) {
586
594
  const key = pluginOutputKey(handle, outputKey);
587
595
  pluginOutputs[key] = createAndRegisterRenderLambda({
588
596
  handle: key,
589
597
  lambda: () => outputFn(new PluginRenderCtx(handle, wrappedInputs)),
590
- withStatus: outputFlags[outputKey]?.withStatus,
598
+ withStatus: true,
591
599
  });
592
600
  }
593
601
  }
@@ -643,6 +651,9 @@ export class BlockModelV3<
643
651
  ),
644
652
  pluginIds: pluginHandles,
645
653
  featureFlags: this.config.featureFlags,
654
+ pluginPublicOutputs: Object.fromEntries(
655
+ pluginHandles.map((handle) => [handle, plugins[handle].model.publicOutputDef]),
656
+ ),
646
657
  },
647
658
  } as any;
648
659
  }
@@ -457,6 +457,7 @@ export class BlockModel<
457
457
  ),
458
458
  pluginIds: [],
459
459
  featureFlags: this.config.featureFlags,
460
+ pluginPublicOutputs: {},
460
461
  },
461
462
  };
462
463
  }
package/src/index.ts CHANGED
@@ -30,7 +30,9 @@ export {
30
30
  type PluginData,
31
31
  type PluginParams,
32
32
  type PluginOutputs,
33
+ type PluginPublicOutputs,
33
34
  type PluginConfig,
35
+ type PublicOutputFieldDef,
34
36
  PluginDataModel,
35
37
  PluginDataModelBuilder,
36
38
  PluginInstance,
@@ -706,6 +706,28 @@ describe("deriveDistinctLabels v2 — linker path & qualifications", () => {
706
706
  expect(deriveDistinctLabels(entries)).toEqual(["Read counts", "Read counts via Sample mapper"]);
707
707
  });
708
708
 
709
+ test("linker suffix is NOT added to records whose native label is already unique, even when other records collide", () => {
710
+ // Repro for "Cluster Id via Clone to cluster link" bug:
711
+ // - "Representative Sequence" exists both as a direct column and via a linker → collision
712
+ // forces algorithm to include LINKER_TYPE in the type set.
713
+ // - As a side effect, every linked entry gets the "via …" suffix appended — including
714
+ // ones whose native label ("Cluster Id") is unique and needs no disambiguation.
715
+ const linker = linkerSpec("Clone to cluster link");
716
+ const entries: Entry[] = [
717
+ { spec: labeledSpec("Representative Sequence", "rep_direct") },
718
+ {
719
+ spec: labeledSpec("Representative Sequence", "rep_linked"),
720
+ linkerPath: [{ spec: linker }],
721
+ },
722
+ { spec: labeledSpec("Cluster Id", "cluster_id"), linkerPath: [{ spec: linker }] },
723
+ ];
724
+ expect(deriveDistinctLabels(entries)).toEqual([
725
+ "Representative Sequence",
726
+ "Representative Sequence via Clone to cluster link",
727
+ "Cluster Id",
728
+ ]);
729
+ });
730
+
709
731
  test("two linker paths → both get distinguishing via-suffix", () => {
710
732
  const s = labeledSpec("Counts");
711
733
  const entries: Entry[] = [
@@ -152,7 +152,16 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
152
152
  forcedSet,
153
153
  separator,
154
154
  );
155
- return build(minimized, false) ?? throwError("Failed to derive unique labels");
155
+ const minimizedLabels =
156
+ build(minimized, false) ?? throwError("Failed to derive unique labels");
157
+ return dropRedundantLinkerSuffix(
158
+ records,
159
+ minimized,
160
+ forceTraceElements,
161
+ forcedSet,
162
+ separator,
163
+ minimizedLabels,
164
+ );
156
165
  }
157
166
 
158
167
  additionalType++;
@@ -171,7 +180,15 @@ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptio
171
180
  forcedSet,
172
181
  separator,
173
182
  );
174
- return build(minimized, true) ?? throwError("Failed to derive unique labels");
183
+ const minimizedLabels = build(minimized, true) ?? throwError("Failed to derive unique labels");
184
+ return dropRedundantLinkerSuffix(
185
+ records,
186
+ minimized,
187
+ forceTraceElements,
188
+ forcedSet,
189
+ separator,
190
+ minimizedLabels,
191
+ );
175
192
  }
176
193
 
177
194
  // --- Pure helpers ---
@@ -359,6 +376,44 @@ function classifyTypes(
359
376
  return { mainTypes, secondaryTypes };
360
377
  }
361
378
 
379
+ function renderRecordLabel(
380
+ record: EnrichedRecord,
381
+ includedTypes: Set<string>,
382
+ forceTraceElements: Set<string> | undefined,
383
+ separator: string,
384
+ ): string | undefined {
385
+ const traceParts: string[] = [];
386
+ const anchorParts: string[] = [];
387
+ let linkerLabel: string | undefined;
388
+ let hitLabel: string | undefined;
389
+
390
+ for (const ft of record.fullTrace) {
391
+ if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
392
+ if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
393
+ else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
394
+ else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
395
+ else traceParts.push(ft.label);
396
+ }
397
+
398
+ const isEmpty =
399
+ traceParts.length === 0 &&
400
+ anchorParts.length === 0 &&
401
+ linkerLabel === undefined &&
402
+ hitLabel === undefined;
403
+
404
+ if (isEmpty) return undefined;
405
+
406
+ let label = traceParts.join(separator);
407
+ const append = (part: string) => {
408
+ label = label.length === 0 ? part : `${label} ${part}`;
409
+ };
410
+ if (linkerLabel !== undefined) append(linkerLabel);
411
+ for (const a of anchorParts) append(a);
412
+ if (hitLabel !== undefined) append(hitLabel);
413
+
414
+ return label;
415
+ }
416
+
362
417
  function buildLabels(
363
418
  records: EnrichedRecord[],
364
419
  includedTypes: Set<string>,
@@ -369,43 +424,58 @@ function buildLabels(
369
424
  const result: string[] = [];
370
425
 
371
426
  for (const r of records) {
372
- const traceParts: string[] = [];
373
- const anchorParts: string[] = [];
374
- let linkerLabel: string | undefined;
375
- let hitLabel: string | undefined;
376
-
377
- for (const ft of r.fullTrace) {
378
- if (!(includedTypes.has(ft.fullType) || forceTraceElements?.has(ft.type))) continue;
379
- if (ft.type === LINKER_TYPE) linkerLabel = ft.label;
380
- else if (ft.type === HIT_QUAL_TYPE) hitLabel = ft.label;
381
- else if (isAnchorQualType(ft.type)) anchorParts.push(ft.label);
382
- else traceParts.push(ft.label);
383
- }
384
-
385
- const isEmpty =
386
- traceParts.length === 0 &&
387
- anchorParts.length === 0 &&
388
- linkerLabel === undefined &&
389
- hitLabel === undefined;
390
-
391
- if (isEmpty) {
427
+ const rendered = renderRecordLabel(r, includedTypes, forceTraceElements, separator);
428
+ if (rendered === undefined) {
392
429
  if (!force) return undefined;
393
430
  result.push("Unlabeled");
394
431
  continue;
395
432
  }
433
+ result.push(rendered);
434
+ }
396
435
 
397
- let label = traceParts.join(separator);
398
- const append = (part: string) => {
399
- label = label.length === 0 ? part : `${label} ${part}`;
400
- };
401
- if (linkerLabel !== undefined) append(linkerLabel);
402
- for (const a of anchorParts) append(a);
403
- if (hitLabel !== undefined) append(hitLabel);
436
+ return result;
437
+ }
438
+
439
+ /**
440
+ * Drop the "via …" linker suffix from records whose label is already unique without it.
441
+ *
442
+ * Global minimization may include `LINKER_TYPE_FULL` solely to resolve a collision between a
443
+ * subset of records — but `buildLabels` then renders the suffix on every record that carries a
444
+ * linker trace entry, including ones whose stem is already unique. We strip the suffix where it
445
+ * isn't load-bearing while keeping the symmetric rendering required by `linkerForced` /
446
+ * `forceTraceElements`.
447
+ *
448
+ * Rule: a record's linker suffix is redundant iff its stem (label rendered without LINKER) does
449
+ * not appear anywhere else in the set.
450
+ */
451
+ function dropRedundantLinkerSuffix(
452
+ records: EnrichedRecord[],
453
+ globalTypeSet: Set<string>,
454
+ forceTraceElements: Set<string> | undefined,
455
+ forcedSet: Set<string>,
456
+ separator: string,
457
+ labels: string[],
458
+ ): string[] {
459
+ if (!globalTypeSet.has(LINKER_TYPE_FULL)) return labels;
460
+ if (forcedSet.has(LINKER_TYPE_FULL) || forceTraceElements?.has(LINKER_TYPE)) return labels;
461
+
462
+ const setWithoutLinker = new Set(globalTypeSet);
463
+ setWithoutLinker.delete(LINKER_TYPE_FULL);
404
464
 
405
- result.push(label);
465
+ const stems = records.map((r) =>
466
+ renderRecordLabel(r, setWithoutLinker, forceTraceElements, separator),
467
+ );
468
+
469
+ const stemOccurrences = new Map<string, number>();
470
+ for (const s of stems) {
471
+ if (s !== undefined) stemOccurrences.set(s, (stemOccurrences.get(s) ?? 0) + 1);
406
472
  }
407
473
 
408
- return result;
474
+ return labels.map((label, i) => {
475
+ const stem = stems[i];
476
+ if (stem === undefined) return label;
477
+ return stemOccurrences.get(stem) === 1 ? stem : label;
478
+ });
409
479
  }
410
480
 
411
481
  function countUniqueLabels(result: string[] | undefined): number {
package/src/platforma.ts CHANGED
@@ -14,6 +14,7 @@ import type { ServiceDispatch } from "@milaboratories/pl-model-common";
14
14
  import type { BlockStatePatch } from "./block_state_patch";
15
15
  import type { PluginRecord } from "./block_model";
16
16
  import type { PluginHandle, PluginFactoryLike } from "./plugin_handle";
17
+ import type { PublicOutputFieldDef } from "./plugin_model";
17
18
 
18
19
  /** Defines all methods to interact with the platform environment from within a block UI. @deprecated */
19
20
  export interface PlatformaV1<
@@ -96,6 +97,7 @@ export type BlockModelInfo = {
96
97
  >;
97
98
  pluginIds: PluginHandle[];
98
99
  featureFlags: BlockCodeKnownFeatureFlags;
100
+ pluginPublicOutputs: Record<string, Record<string, PublicOutputFieldDef>>;
99
101
  };
100
102
 
101
103
  export type PlatformaApiVersion = Platforma["apiVersion"];
@@ -150,18 +152,22 @@ export type InferPluginData<Pl, PluginId extends string> =
150
152
  : never;
151
153
 
152
154
  /**
153
- * Map each plugin instance to a type-safe opaque handle branded with normalized phantom.
154
- * Uses the same brand structure as InferPluginHandle only data/params/outputs, no config
155
- * because PluginRecord doesn't carry Config (lost after factory.create()).
155
+ * Derives the UI-facing entry map for a plugin registry.
156
+ * For each plugin instance, produces a typed handle and its public outputs
157
+ * the subset of plugin state accessible to block UI without usePlugin().
156
158
  */
157
- export type InferPluginHandles<T extends Record<string, unknown>> = {
159
+ export type InferPluginUiEntries<T extends Record<string, unknown>> = {
158
160
  readonly [K in keyof T]: T[K] extends PluginRecord<
159
161
  infer Data,
160
162
  infer Params,
161
163
  infer Outputs,
164
+ infer PublicOutputs,
162
165
  infer ModelServices,
163
166
  infer UiServices
164
167
  >
165
- ? { handle: PluginHandle<PluginFactoryLike<Data, Params, Outputs, ModelServices, UiServices>> }
168
+ ? {
169
+ handle: PluginHandle<PluginFactoryLike<Data, Params, Outputs, ModelServices, UiServices>>;
170
+ publicOutputs: PublicOutputs;
171
+ }
166
172
  : never;
167
173
  };
@@ -61,7 +61,7 @@ export type InferFactoryUiServices<F extends PluginFactoryLike> =
61
61
  /**
62
62
  * Derive a typed PluginHandle from a PluginFactory type.
63
63
  * Normalizes the brand to data/params/outputs/services (strips config) so handles
64
- * from InferPluginHandles match handles from InferPluginHandle.
64
+ * from InferPluginUiEntries match handles from InferPluginHandle.
65
65
  */
66
66
  export type InferPluginHandle<F extends PluginFactoryLike> = NonNullable<
67
67
  F extends PluginFactoryLike<