@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
@@ -1,6 +1,11 @@
1
1
  import { DistributiveKeys, UnionToTuples } from "@milaboratories/helpers";
2
- import { type FilterSpec, type FilterSpecLeaf } from "@milaboratories/pl-model-common";
2
+ import {
3
+ RootFilterSpec,
4
+ type FilterSpec,
5
+ type FilterSpecLeaf,
6
+ } from "@milaboratories/pl-model-common";
3
7
  import { traverseFilterSpec } from "./traverse";
8
+ import { InferFilterSpecLeaf } from "@milaboratories/pl-model-common";
4
9
 
5
10
  /** All possible field names that can appear in any FilterSpecLeaf variant. */
6
11
  type FilterSpecLeafKey = DistributiveKeys<FilterSpecLeaf<string>>;
@@ -41,23 +46,26 @@ function distillLeaf(node: Record<string, unknown>): FilterSpecLeaf<string> {
41
46
  * Strips non-FilterSpec metadata (whitelist approach) and removes
42
47
  * unfilled leaves (type is undefined or any required field is undefined).
43
48
  */
44
- export function distillFilterSpec<T extends FilterSpecLeaf<unknown>>(
45
- filter: null | undefined | FilterSpec<T, unknown, unknown>,
46
- ): null | FilterSpec<T> {
49
+ export function distillFilterSpec<
50
+ FS extends FilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>,
51
+ R extends FS extends RootFilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>
52
+ ? RootFilterSpec<InferFilterSpecLeaf<FS>>
53
+ : FilterSpec<InferFilterSpecLeaf<FS>>,
54
+ >(filter: null | undefined | FS): null | R {
47
55
  if (filter == null) return null;
48
- return traverseFilterSpec(filter, {
56
+ return traverseFilterSpec<FS, null | R>(filter, {
49
57
  leaf: (leaf) => {
50
58
  if (!isFilledLeaf(leaf as Record<string, unknown>)) return null;
51
- return distillLeaf(leaf as Record<string, unknown>) as FilterSpec<T>;
59
+ return distillLeaf(leaf as Record<string, unknown>) as R;
52
60
  },
53
61
  and: (results) => {
54
- const filtered = results.filter((f): f is FilterSpec<T> => f !== null);
55
- return filtered.length === 0 ? null : { type: "and", filters: filtered };
62
+ const filtered = results.filter((f): f is NonNullable<typeof f> => f !== null);
63
+ return filtered.length === 0 ? null : ({ type: "and", filters: filtered } as R);
56
64
  },
57
65
  or: (results) => {
58
- const filtered = results.filter((f): f is FilterSpec<T> => f !== null);
59
- return filtered.length === 0 ? null : { type: "or", filters: filtered };
66
+ const filtered = results.filter((f): f is NonNullable<typeof f> => f !== null);
67
+ return filtered.length === 0 ? null : ({ type: "or", filters: filtered } as R);
60
68
  },
61
- not: (result) => (result === null ? null : { type: "not", filter: result }),
69
+ not: (result) => (result === null ? null : ({ type: "not", filter: result } as R)),
62
70
  });
63
71
  }
@@ -1,4 +1,24 @@
1
- import type { FilterSpecLeaf, FilterSpecNode } from "@milaboratories/pl-model-common";
1
+ import type {
2
+ FilterSpec,
3
+ FilterSpecLeaf,
4
+ FilterSpecNode,
5
+ InferFilterSpecLeafColumn,
6
+ } from "@milaboratories/pl-model-common";
7
+ import type {
8
+ InferFilterSpecCommonLeaf,
9
+ InferFilterSpecLeaf,
10
+ } from "@milaboratories/pl-model-common";
11
+
12
+ export type FilterSpecVisitor<LeafArg, R> = {
13
+ /** Handle a leaf filter node. */
14
+ leaf: (leaf: LeafArg) => R;
15
+ /** Handle an AND node after children have been traversed. */
16
+ and: (results: R[]) => R;
17
+ /** Handle an OR node after children have been traversed. */
18
+ or: (results: R[]) => R;
19
+ /** Handle a NOT node after the inner filter has been traversed. */
20
+ not: (result: R) => R;
21
+ };
2
22
 
3
23
  /**
4
24
  * Recursively traverses a FilterSpec tree bottom-up, applying visitor callbacks.
@@ -11,50 +31,50 @@ import type { FilterSpecLeaf, FilterSpecNode } from "@milaboratories/pl-model-co
11
31
  * 2. Apply the corresponding visitor callback with already-traversed children
12
32
  * 3. For leaf nodes, call `leaf` directly
13
33
  */
14
- export function traverseFilterSpec<Leaf extends FilterSpecLeaf<unknown>, CommonNode, CommonLeaf, R>(
15
- filter: FilterSpecNode<Leaf, CommonNode, CommonLeaf>,
16
- visitor: {
17
- /** Handle a leaf filter node. */
18
- leaf: (leaf: CommonLeaf & Leaf) => R;
19
- /** Handle an AND node after children have been traversed. */
20
- and: (results: R[]) => R;
21
- /** Handle an OR node after children have been traversed. */
22
- or: (results: R[]) => R;
23
- /** Handle a NOT node after the inner filter has been traversed. */
24
- not: (result: R) => R;
25
- },
34
+ export function traverseFilterSpec<
35
+ F extends FilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>,
36
+ R,
37
+ >(
38
+ filter: F,
39
+ visitor: FilterSpecVisitor<InferFilterSpecCommonLeaf<F> & InferFilterSpecLeaf<F>, R>,
40
+ ): R {
41
+ return traverseFilterSpecImpl(filter, visitor as FilterSpecVisitor<unknown, R>);
42
+ }
43
+ /** Internal implementation with simple generics for clean recursion. */
44
+ function traverseFilterSpecImpl<R>(
45
+ filter: FilterSpecNode<FilterSpecLeaf<unknown>, unknown, unknown>,
46
+ visitor: FilterSpecVisitor<unknown, R>,
26
47
  ): R {
27
48
  switch (filter.type) {
28
49
  case "and":
29
50
  return visitor.and(
30
51
  filter.filters
31
52
  .filter((f) => f.type !== undefined)
32
- .map((f) => traverseFilterSpec(f, visitor)),
53
+ .map((f) => traverseFilterSpecImpl(f, visitor)),
33
54
  );
34
55
  case "or":
35
56
  return visitor.or(
36
57
  filter.filters
37
58
  .filter((f) => f.type !== undefined)
38
- .map((f) => traverseFilterSpec(f, visitor)),
59
+ .map((f) => traverseFilterSpecImpl(f, visitor)),
39
60
  );
40
61
  case "not":
41
- return visitor.not(traverseFilterSpec(filter.filter, visitor));
62
+ return visitor.not(traverseFilterSpecImpl(filter.filter, visitor));
42
63
  default:
43
- return visitor.leaf(filter as CommonLeaf & Leaf);
64
+ return visitor.leaf(filter);
44
65
  }
45
66
  }
46
67
 
47
68
  /** Collects all column references (`column` and `rhs` fields) from filter leaves. */
48
69
  export function collectFilterSpecColumns<
49
- Leaf extends FilterSpecLeaf<unknown>,
50
- CommonNode,
51
- CommonLeaf,
52
- >(filter: FilterSpecNode<Leaf, CommonNode, CommonLeaf>): string[] {
70
+ F extends FilterSpec<FilterSpecLeaf<unknown>, unknown, unknown>,
71
+ R extends InferFilterSpecLeafColumn<F> = InferFilterSpecLeafColumn<F>,
72
+ >(filter: F): R[] {
53
73
  return traverseFilterSpec(filter, {
54
74
  leaf: (leaf) => {
55
- const cols: string[] = [];
56
- if ("column" in leaf && leaf.column !== undefined) cols.push(leaf.column as string);
57
- if ("rhs" in leaf && leaf.rhs !== undefined) cols.push(leaf.rhs as string);
75
+ const cols: R[] = [];
76
+ if ("column" in leaf && leaf.column !== undefined) cols.push(leaf.column as R);
77
+ if ("rhs" in leaf && leaf.rhs !== undefined) cols.push(leaf.rhs as R);
58
78
  return cols;
59
79
  },
60
80
  and: (results) => results.flat(),
package/src/index.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  export * from "./block_state_patch";
2
2
  export * from "./block_state_util";
3
3
  export * from "./block_storage";
4
- export * from "./builder";
4
+ export * from "./block_storage_facade";
5
+ export * from "./block_model_legacy";
5
6
  export { BlockModelV3 } from "./block_model";
7
+ export type { PluginInstance, ParamsInput } from "./block_model";
6
8
  export {
7
9
  DataModel,
8
10
  DataModelBuilder,
9
11
  DataUnrecoverableError,
10
12
  isDataUnrecoverableError,
11
- defineDataVersions,
12
13
  defaultRecover,
13
14
  makeDataVersioned,
14
15
  } from "./block_migrations";
16
+ export type { LegacyV1State } from "./block_migrations";
17
+ export { PluginModel } from "./plugin_model";
18
+ export type { PluginRenderCtx } from "./plugin_model";
15
19
  export * from "./bconfig";
16
20
  export * from "./components";
17
21
  export * from "./config";
@@ -19,7 +23,7 @@ export * from "./pframe";
19
23
  export * from "./platforma";
20
24
  export * from "./ref_util";
21
25
  export * from "./render";
22
- export * from "./sdk_info";
26
+ export * from "./version";
23
27
  export * from "./raw_globals";
24
28
  export * from "./block_api_v1";
25
29
  export * from "./block_api_v2";
package/src/platforma.ts CHANGED
@@ -7,8 +7,9 @@ import type {
7
7
  DriverKit,
8
8
  OutputWithStatus,
9
9
  } from "@milaboratories/pl-model-common";
10
- import type { SdkInfo } from "./sdk_info";
10
+ import type { SdkInfo } from "./version";
11
11
  import type { BlockStatePatch } from "./block_state_patch";
12
+ import type { PluginInstance } from "./block_model";
12
13
 
13
14
  /** Defines all methods to interact with the platform environment from within a block UI. @deprecated */
14
15
  export interface PlatformaV1<
@@ -43,18 +44,21 @@ export interface PlatformaV2<
43
44
  }
44
45
 
45
46
  export interface PlatformaV3<
47
+ Data = unknown,
46
48
  Args = unknown,
47
49
  Outputs extends Record<string, OutputWithStatus<unknown>> = Record<
48
50
  string,
49
51
  OutputWithStatus<unknown>
50
52
  >,
51
- Data = unknown,
52
53
  Href extends `/${string}` = `/${string}`,
54
+ Plugins extends Record<string, unknown> = Record<string, unknown>,
53
55
  >
54
- extends BlockApiV3<Args, Outputs, Data, Href>, DriverKit {
56
+ extends BlockApiV3<Data, Args, Outputs, Href>, DriverKit {
55
57
  /** Information about SDK version current platforma environment was compiled with. */
56
58
  readonly sdkInfo: SdkInfo;
57
59
  readonly apiVersion: 3;
60
+ /** @internal Type brand for plugin type inference. Not used at runtime. */
61
+ readonly __pluginsBrand?: Plugins;
58
62
  }
59
63
 
60
64
  export type Platforma<
@@ -63,12 +67,12 @@ export type Platforma<
63
67
  string,
64
68
  OutputWithStatus<unknown>
65
69
  >,
66
- UiState = unknown,
70
+ UiStateOrData = unknown,
67
71
  Href extends `/${string}` = `/${string}`,
68
72
  > =
69
- | PlatformaV1<Args, Outputs, UiState, Href>
70
- | PlatformaV2<Args, Outputs, UiState, Href>
71
- | PlatformaV3<Args, Outputs, UiState, Href>;
73
+ | PlatformaV1<Args, Outputs, UiStateOrData, Href>
74
+ | PlatformaV2<Args, Outputs, UiStateOrData, Href>
75
+ | PlatformaV3<UiStateOrData, Args, Outputs, Href>;
72
76
 
73
77
  export type PlatformaExtended<Pl extends Platforma = Platforma> = Pl & {
74
78
  blockModelInfo: BlockModelInfo;
@@ -106,6 +110,7 @@ export type InferHrefType<Pl extends Platforma> =
106
110
  export type PlatformaFactory = (config: { sdkVersion: string }) => Platforma;
107
111
 
108
112
  export type InferBlockState<Pl extends Platforma> = BlockStateV3<
113
+ InferDataType<Pl>,
109
114
  InferOutputsType<Pl>,
110
115
  InferHrefType<Pl>
111
116
  >;
@@ -116,3 +121,17 @@ export type InferBlockStatePatch<Pl extends Platforma> = BlockStatePatch<
116
121
  InferUiState<Pl>,
117
122
  InferHrefType<Pl>
118
123
  >;
124
+
125
+ /** Extract plugin IDs as a string literal union from a Platforma type. */
126
+ export type InferPluginNames<Pl> =
127
+ Pl extends PlatformaV3<any, any, any, any, infer P> ? string & keyof P : never;
128
+
129
+ /** Extract the Data type for a specific plugin by its ID. */
130
+ export type InferPluginData<Pl, PluginId extends string> =
131
+ Pl extends PlatformaV3<any, any, any, any, infer P>
132
+ ? PluginId extends keyof P
133
+ ? P[PluginId] extends PluginInstance<infer D, any, any>
134
+ ? D
135
+ : never
136
+ : never
137
+ : never;
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { PluginModel } from "./plugin_model";
3
+ import type { PluginRenderCtx } from "./plugin_model";
4
+ import { DataModelBuilder } from "./block_migrations";
5
+ import { DATA_MODEL_DEFAULT_VERSION, type PluginName } from "./block_storage";
6
+ import type { ResultPool } from "./render";
7
+
8
+ // =============================================================================
9
+ // Test Fixtures
10
+ // =============================================================================
11
+
12
+ type Data = { count: number; label: string };
13
+
14
+ const dataModelChain = new DataModelBuilder().from<Data>(DATA_MODEL_DEFAULT_VERSION);
15
+
16
+ // Mock ResultPool for testing
17
+ const mockResultPool = {} as ResultPool;
18
+
19
+ // =============================================================================
20
+ // Tests
21
+ // =============================================================================
22
+
23
+ describe("PluginModel", () => {
24
+ it("creates PluginModel with required fields", () => {
25
+ const factory = PluginModel.define<Data>({
26
+ name: "testPlugin" as PluginName,
27
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
28
+ }).build();
29
+
30
+ const plugin = factory.create();
31
+ expect(plugin.name).toBe("testPlugin");
32
+ expect(plugin.outputs).toEqual({});
33
+ });
34
+
35
+ it("creates PluginModel when calling create() with config", () => {
36
+ type Config = { initialCount: number };
37
+
38
+ const factory = PluginModel.define<Data, undefined, Config>({
39
+ name: "factoryPlugin" as PluginName,
40
+ data: (cfg) => dataModelChain.init(() => ({ count: cfg.initialCount, label: "initialized" })),
41
+ }).build();
42
+
43
+ const plugin = factory.create({ initialCount: 100 });
44
+ expect(plugin.name).toBe("factoryPlugin");
45
+ expect(plugin.dataModel.initialData()).toEqual({ count: 100, label: "initialized" });
46
+ });
47
+
48
+ it("adds single output", () => {
49
+ const factory = PluginModel.define<Data, { multiplier: number }>({
50
+ name: "singleOutput" as PluginName,
51
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
52
+ })
53
+ .output("doubled", (ctx) => ctx.data.count * ctx.params.multiplier)
54
+ .build();
55
+
56
+ const plugin = factory.create();
57
+ expect(Object.keys(plugin.outputs)).toEqual(["doubled"]);
58
+ });
59
+
60
+ it("accumulates multiple outputs", () => {
61
+ const factory = PluginModel.define<Data, { prefix: string }>({
62
+ name: "multiOutput" as PluginName,
63
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
64
+ })
65
+ .output("formattedCount", (ctx) => `${ctx.params.prefix}${ctx.data.count}`)
66
+ .output("upperLabel", (ctx) => ctx.data.label.toUpperCase())
67
+ .output("isReady", (ctx) => ctx.data.count > 0)
68
+ .build();
69
+
70
+ const plugin = factory.create();
71
+ expect(Object.keys(plugin.outputs).sort()).toEqual(["formattedCount", "isReady", "upperLabel"]);
72
+ });
73
+
74
+ it("executes output functions with correct context", () => {
75
+ const factory = PluginModel.define<Data, { factor: number }>({
76
+ name: "contextTest" as PluginName,
77
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
78
+ })
79
+ .output("computed", (ctx) => ctx.data.count * ctx.params.factor)
80
+ .build();
81
+
82
+ const plugin = factory.create();
83
+
84
+ const ctx: PluginRenderCtx<Data, { factor: number }> = {
85
+ data: { count: 5, label: "" },
86
+ params: { factor: 3 },
87
+ resultPool: mockResultPool,
88
+ };
89
+
90
+ const result = plugin.outputs.computed(ctx);
91
+ expect(result).toBe(15);
92
+ });
93
+
94
+ it("allows outputs to access resultPool", () => {
95
+ const factory = PluginModel.define<Data>({
96
+ name: "resultPoolTest" as PluginName,
97
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
98
+ })
99
+ .output("hasResultPool", (ctx) => ctx.resultPool !== undefined)
100
+ .build();
101
+
102
+ const plugin = factory.create();
103
+
104
+ const ctx: PluginRenderCtx<Data> = {
105
+ data: { count: 0, label: "" },
106
+ params: undefined,
107
+ resultPool: mockResultPool,
108
+ };
109
+
110
+ expect(plugin.outputs.hasResultPool(ctx)).toBe(true);
111
+ });
112
+
113
+ it("returns valid PluginModel from factory.create()", () => {
114
+ const factory = PluginModel.define<Data, { items: string[] }, { option: boolean }>({
115
+ name: "completePlugin" as PluginName,
116
+ data: () => dataModelChain.init(() => ({ count: -1, label: "" })),
117
+ })
118
+ .output("currentItem", (ctx) => ctx.params.items[ctx.data.count])
119
+ .output("hasSelection", (ctx) => ctx.data.count >= 0)
120
+ .build();
121
+
122
+ const plugin = factory.create({ option: true });
123
+ expect(plugin.name).toBe("completePlugin");
124
+ expect(Object.keys(plugin.outputs).sort()).toEqual(["currentItem", "hasSelection"]);
125
+ });
126
+
127
+ it("allows creating plugin without outputs", () => {
128
+ const factory = PluginModel.define<Data>({
129
+ name: "noOutputs" as PluginName,
130
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
131
+ }).build();
132
+
133
+ const plugin = factory.create();
134
+ expect(plugin.outputs).toEqual({});
135
+ });
136
+
137
+ it("passes config to data model factory for initialization", () => {
138
+ type Config = { defaultCount: number; defaultLabel: string };
139
+
140
+ const factory = PluginModel.define<Data, undefined, Config>({
141
+ name: "configInitPlugin" as PluginName,
142
+ data: (cfg) =>
143
+ dataModelChain.init(() => ({ count: cfg.defaultCount, label: cfg.defaultLabel })),
144
+ }).build();
145
+
146
+ const plugin = factory.create({ defaultCount: 10, defaultLabel: "default" });
147
+ expect(plugin.dataModel.initialData()).toEqual({ count: 10, label: "default" });
148
+ });
149
+
150
+ it("does not modify original builder when chaining", () => {
151
+ const basePlugin = PluginModel.define<Data>({
152
+ name: "immutableTest" as PluginName,
153
+ data: () => dataModelChain.init(() => ({ count: 0, label: "" })),
154
+ });
155
+
156
+ const pluginWithOutput1 = basePlugin.output("first", (ctx) => ctx.data.count);
157
+ const pluginWithOutput2 = basePlugin.output("second", (ctx) => ctx.data.count * 2);
158
+
159
+ const factory1 = pluginWithOutput1.build();
160
+ const factory2 = pluginWithOutput2.build();
161
+
162
+ const plugin1 = factory1.create(undefined);
163
+ const plugin2 = factory2.create(undefined);
164
+
165
+ expect(Object.keys(plugin1.outputs)).toEqual(["first"]);
166
+ expect(Object.keys(plugin2.outputs)).toEqual(["second"]);
167
+ });
168
+ });
@@ -0,0 +1,242 @@
1
+ /**
2
+ * PluginModel - Builder for creating plugin types with data model and outputs.
3
+ *
4
+ * Plugins are UI components with their own model logic and persistent state.
5
+ * Block developers register plugin instances via BlockModelV3.plugin() method.
6
+ *
7
+ * @module plugin_model
8
+ */
9
+
10
+ import type { DataModel } from "./block_migrations";
11
+ import type { PluginName } from "./block_storage";
12
+ import type { ResultPool } from "./render";
13
+
14
+ /** Symbol for internal builder creation method */
15
+ const FROM_BUILDER = Symbol("fromBuilder");
16
+
17
+ // =============================================================================
18
+ // Plugin Render Context
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Context passed to plugin output functions.
23
+ * Provides access to plugin's persistent data, params derived from block context,
24
+ * and the result pool for accessing workflow outputs.
25
+ */
26
+ export interface PluginRenderCtx<Data, Params = undefined> {
27
+ /** Plugin's persistent data */
28
+ readonly data: Data;
29
+ /** Params derived from block's RenderCtx */
30
+ readonly params: Params;
31
+ /** Result pool for accessing workflow outputs */
32
+ readonly resultPool: ResultPool;
33
+ }
34
+
35
+ // =============================================================================
36
+ // Plugin Type
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Configured plugin instance returned by PluginModelFactory.create().
41
+ * Contains the plugin's name, data model, and output definitions.
42
+ */
43
+ export class PluginModel<Data = unknown, Params = undefined, Outputs = {}> {
44
+ /** Globally unique plugin name */
45
+ readonly name: PluginName;
46
+ /** Data model instance for this plugin */
47
+ readonly dataModel: DataModel<Data>;
48
+ /** Output definitions - functions that compute outputs from plugin context */
49
+ readonly outputs: { [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K] };
50
+
51
+ private constructor(input: {
52
+ name: PluginName;
53
+ dataModel: DataModel<Data>;
54
+ outputs: { [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K] };
55
+ }) {
56
+ this.name = input.name;
57
+ this.dataModel = input.dataModel;
58
+ this.outputs = input.outputs;
59
+ }
60
+
61
+ /**
62
+ * Internal method for creating PluginModel from factory.
63
+ * Uses Symbol key to prevent external access.
64
+ * @internal
65
+ */
66
+ static [FROM_BUILDER]<D, P, O>(input: {
67
+ name: PluginName;
68
+ dataModel: DataModel<D>;
69
+ outputs: { [K in keyof O]: (ctx: PluginRenderCtx<D, P>) => O[K] };
70
+ }): PluginModel<D, P, O> {
71
+ return new this<D, P, O>(input);
72
+ }
73
+
74
+ /**
75
+ * Creates a new PluginModelBuilder for building plugin definitions.
76
+ *
77
+ * @param options.name - Globally unique plugin name
78
+ * @param options.data - Factory function that creates the data model from config
79
+ * @returns PluginModelBuilder for chaining output definitions
80
+ *
81
+ * @example
82
+ * const dataModelChain = new DataModelBuilder().from<MyData>(DATA_MODEL_DEFAULT_VERSION);
83
+ *
84
+ * const myPlugin = PluginModel.define<MyData, MyParams, MyConfig>({
85
+ * name: 'myPlugin' as PluginName,
86
+ * data: (cfg) => dataModelChain.init(() => ({ value: cfg.defaultValue })),
87
+ * })
88
+ * .output('computed', (ctx) => ctx.data.value * ctx.params.multiplier)
89
+ * .build();
90
+ */
91
+ static define<Data, Params = undefined, Config = undefined>(options: {
92
+ name: PluginName;
93
+ data: (config?: Config) => DataModel<Data>;
94
+ }): PluginModelBuilder<Data, Params, Config> {
95
+ return PluginModelBuilder[FROM_BUILDER]<Data, Params, Config>(options);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Plugin factory returned by PluginModelBuilder.build().
101
+ * Call create() with config to get a configured PluginModel instance.
102
+ */
103
+ class PluginModelFactory<Data, Params, Config, Outputs> {
104
+ private readonly name: PluginName;
105
+ private readonly data: (config?: Config) => DataModel<Data>;
106
+ private readonly outputs: {
107
+ [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K];
108
+ };
109
+
110
+ constructor(input: {
111
+ name: PluginName;
112
+ data: (config?: Config) => DataModel<Data>;
113
+ outputs: { [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K] };
114
+ }) {
115
+ this.name = input.name;
116
+ this.data = input.data;
117
+ this.outputs = input.outputs;
118
+ }
119
+
120
+ /** Create a configured PluginModel instance */
121
+ create(config?: Config): PluginModel<Data, Params, Outputs> {
122
+ return PluginModel[FROM_BUILDER]<Data, Params, Outputs>({
123
+ name: this.name,
124
+ dataModel: this.data(config),
125
+ outputs: this.outputs,
126
+ });
127
+ }
128
+ }
129
+
130
+ // =============================================================================
131
+ // Plugin Model Builder
132
+ // =============================================================================
133
+
134
+ /**
135
+ * Builder for creating PluginType with type-safe output definitions.
136
+ *
137
+ * Use `PluginModel.define()` to create a builder instance.
138
+ *
139
+ * @typeParam Data - Plugin's persistent data type
140
+ * @typeParam Params - Params derived from block's RenderCtx (optional)
141
+ * @typeParam Config - Static configuration passed to plugin factory (optional)
142
+ * @typeParam Outputs - Accumulated output types
143
+ *
144
+ * @example
145
+ * const dataModelChain = new DataModelBuilder().from<TableData>(DATA_MODEL_DEFAULT_VERSION);
146
+ *
147
+ * const dataTable = PluginModel.define<TableData, TableParams, TableConfig>({
148
+ * name: 'dataTable' as PluginName,
149
+ * data: (cfg) => {
150
+ * return dataModelChain.init(() => ({ state: createInitialState(cfg.ops) }));
151
+ * },
152
+ * })
153
+ * .output('model', (ctx) => createTableModel(ctx))
154
+ * .build();
155
+ */
156
+ class PluginModelBuilder<
157
+ Data,
158
+ Params = undefined,
159
+ Config = undefined,
160
+ Outputs extends Record<string, unknown> = {},
161
+ > {
162
+ private readonly name: PluginName;
163
+ private readonly data: (config?: Config) => DataModel<Data>;
164
+ private readonly outputs: {
165
+ [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K];
166
+ };
167
+
168
+ private constructor(input: {
169
+ name: PluginName;
170
+ data: (config?: Config) => DataModel<Data>;
171
+ outputs?: { [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K] };
172
+ }) {
173
+ this.name = input.name;
174
+ this.data = input.data;
175
+ this.outputs =
176
+ input.outputs ??
177
+ ({} as { [K in keyof Outputs]: (ctx: PluginRenderCtx<Data, Params>) => Outputs[K] });
178
+ }
179
+
180
+ /**
181
+ * Internal method for creating PluginModelBuilder.
182
+ * Uses Symbol key to prevent external access.
183
+ * @internal
184
+ */
185
+ static [FROM_BUILDER]<D, P, C, O extends Record<string, unknown> = {}>(input: {
186
+ name: PluginName;
187
+ data: (config?: C) => DataModel<D>;
188
+ outputs?: { [K in keyof O]: (ctx: PluginRenderCtx<D, P>) => O[K] };
189
+ }): PluginModelBuilder<D, P, C, O> {
190
+ return new this<D, P, C, O>(input);
191
+ }
192
+
193
+ /**
194
+ * Adds an output to the plugin.
195
+ *
196
+ * @param key - Output name
197
+ * @param fn - Function that computes the output value from plugin context
198
+ * @returns PluginModel with the new output added
199
+ *
200
+ * @example
201
+ * .output('model', (ctx) => createModel(ctx.params.columns, ctx.data.state))
202
+ * .output('isReady', (ctx) => ctx.params.columns !== undefined)
203
+ */
204
+ output<const Key extends string, T>(
205
+ key: Key,
206
+ fn: (ctx: PluginRenderCtx<Data, Params>) => T,
207
+ ): PluginModelBuilder<Data, Params, Config, Outputs & { [K in Key]: T }> {
208
+ return new PluginModelBuilder<Data, Params, Config, Outputs & { [K in Key]: T }>({
209
+ name: this.name,
210
+ data: this.data,
211
+ outputs: {
212
+ ...this.outputs,
213
+ [key]: fn,
214
+ } as {
215
+ [K in keyof (Outputs & { [P in Key]: T })]: (
216
+ ctx: PluginRenderCtx<Data, Params>,
217
+ ) => (Outputs & { [P in Key]: T })[K];
218
+ },
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Finalizes the plugin definition and returns a ConfigurablePluginModel.
224
+ *
225
+ * @returns Callable plugin factory that accepts config and returns PluginModel
226
+ *
227
+ * @example
228
+ * const myPlugin = new PluginModelBuilder('myPlugin', () => dataModel)
229
+ * .output('value', (ctx) => ctx.data.value)
230
+ * .build();
231
+ *
232
+ * // Later, call create() with config to get a configured instance:
233
+ * const configured = myPlugin.create({ defaultValue: 'test' });
234
+ */
235
+ build(): PluginModelFactory<Data, Params, Config, Outputs> {
236
+ return new PluginModelFactory<Data, Params, Config, Outputs>({
237
+ name: this.name,
238
+ data: this.data,
239
+ outputs: this.outputs,
240
+ });
241
+ }
242
+ }