@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.
- package/dist/bconfig/normalization.cjs +8 -1
- package/dist/bconfig/normalization.cjs.map +1 -1
- package/dist/bconfig/normalization.d.ts.map +1 -1
- package/dist/bconfig/normalization.js +8 -1
- package/dist/bconfig/normalization.js.map +1 -1
- package/dist/block_api_v3.d.ts +2 -2
- package/dist/block_api_v3.d.ts.map +1 -1
- package/dist/block_migrations.cjs +246 -214
- package/dist/block_migrations.cjs.map +1 -1
- package/dist/block_migrations.d.ts +180 -158
- package/dist/block_migrations.d.ts.map +1 -1
- package/dist/block_migrations.js +247 -214
- package/dist/block_migrations.js.map +1 -1
- package/dist/block_model.cjs +85 -35
- package/dist/block_model.cjs.map +1 -1
- package/dist/block_model.d.ts +66 -38
- package/dist/block_model.d.ts.map +1 -1
- package/dist/block_model.js +86 -36
- package/dist/block_model.js.map +1 -1
- package/dist/{builder.cjs → block_model_legacy.cjs} +2 -2
- package/dist/block_model_legacy.cjs.map +1 -0
- package/dist/{builder.d.ts → block_model_legacy.d.ts} +1 -1
- package/dist/block_model_legacy.d.ts.map +1 -0
- package/dist/{builder.js → block_model_legacy.js} +2 -2
- package/dist/block_model_legacy.js.map +1 -0
- package/dist/block_state_patch.d.ts +11 -1
- package/dist/block_state_patch.d.ts.map +1 -1
- package/dist/block_storage.cjs +126 -109
- package/dist/block_storage.cjs.map +1 -1
- package/dist/block_storage.d.ts +109 -112
- package/dist/block_storage.d.ts.map +1 -1
- package/dist/block_storage.js +126 -101
- package/dist/block_storage.js.map +1 -1
- package/dist/block_storage_callbacks.cjs +227 -0
- package/dist/block_storage_callbacks.cjs.map +1 -0
- package/dist/block_storage_callbacks.d.ts +113 -0
- package/dist/block_storage_callbacks.d.ts.map +1 -0
- package/dist/block_storage_callbacks.js +218 -0
- package/dist/block_storage_callbacks.js.map +1 -0
- package/dist/block_storage_facade.cjs +104 -0
- package/dist/block_storage_facade.cjs.map +1 -0
- package/dist/block_storage_facade.d.ts +168 -0
- package/dist/block_storage_facade.d.ts.map +1 -0
- package/dist/block_storage_facade.js +99 -0
- package/dist/block_storage_facade.js.map +1 -0
- package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
- package/dist/components/PlDataTable/state-migration.js.map +1 -1
- package/dist/components/PlDataTable/table.cjs +11 -2
- package/dist/components/PlDataTable/table.cjs.map +1 -1
- package/dist/components/PlDataTable/table.d.ts.map +1 -1
- package/dist/components/PlDataTable/table.js +12 -3
- package/dist/components/PlDataTable/table.js.map +1 -1
- package/dist/components/PlDataTable/v5.d.ts +7 -4
- package/dist/components/PlDataTable/v5.d.ts.map +1 -1
- package/dist/filters/converters/filterToQuery.cjs +3 -4
- package/dist/filters/converters/filterToQuery.cjs.map +1 -1
- package/dist/filters/converters/filterToQuery.d.ts +1 -1
- package/dist/filters/converters/filterToQuery.d.ts.map +1 -1
- package/dist/filters/converters/filterToQuery.js +3 -4
- package/dist/filters/converters/filterToQuery.js.map +1 -1
- package/dist/filters/distill.cjs.map +1 -1
- package/dist/filters/distill.d.ts +3 -2
- package/dist/filters/distill.d.ts.map +1 -1
- package/dist/filters/distill.js.map +1 -1
- package/dist/filters/traverse.cjs +7 -3
- package/dist/filters/traverse.cjs.map +1 -1
- package/dist/filters/traverse.d.ts +14 -12
- package/dist/filters/traverse.d.ts.map +1 -1
- package/dist/filters/traverse.js +7 -3
- package/dist/filters/traverse.js.map +1 -1
- package/dist/index.cjs +13 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/platforma.d.ts +11 -4
- package/dist/platforma.d.ts.map +1 -1
- package/dist/plugin_model.cjs +171 -0
- package/dist/plugin_model.cjs.map +1 -0
- package/dist/plugin_model.d.ts +162 -0
- package/dist/plugin_model.d.ts.map +1 -0
- package/dist/plugin_model.js +169 -0
- package/dist/plugin_model.js.map +1 -0
- package/dist/render/api.cjs +20 -21
- package/dist/render/api.cjs.map +1 -1
- package/dist/render/api.d.ts +8 -8
- package/dist/render/api.d.ts.map +1 -1
- package/dist/render/api.js +20 -21
- package/dist/render/api.js.map +1 -1
- package/dist/render/internal.cjs.map +1 -1
- package/dist/render/internal.d.ts +1 -1
- package/dist/render/internal.d.ts.map +1 -1
- package/dist/render/internal.js.map +1 -1
- package/dist/version.cjs +4 -0
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +4 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -6
- package/src/bconfig/normalization.ts +8 -1
- package/src/block_api_v3.ts +2 -2
- package/src/block_migrations.test.ts +141 -171
- package/src/block_migrations.ts +300 -285
- package/src/block_model.ts +205 -95
- package/src/{builder.ts → block_model_legacy.ts} +1 -1
- package/src/block_state_patch.ts +13 -1
- package/src/block_storage.test.ts +283 -95
- package/src/block_storage.ts +199 -188
- package/src/block_storage_callbacks.ts +326 -0
- package/src/block_storage_facade.ts +199 -0
- package/src/components/PlDataTable/state-migration.ts +4 -4
- package/src/components/PlDataTable/table.ts +16 -3
- package/src/components/PlDataTable/v5.ts +9 -5
- package/src/filters/converters/filterToQuery.ts +8 -7
- package/src/filters/distill.ts +19 -11
- package/src/filters/traverse.ts +44 -24
- package/src/index.ts +7 -3
- package/src/platforma.ts +26 -7
- package/src/plugin_model.test.ts +168 -0
- package/src/plugin_model.ts +242 -0
- package/src/render/api.ts +26 -24
- package/src/render/internal.ts +1 -1
- package/src/typing.test.ts +1 -1
- package/src/version.ts +8 -0
- package/dist/block_storage_vm.cjs +0 -262
- package/dist/block_storage_vm.cjs.map +0 -1
- package/dist/block_storage_vm.d.ts +0 -59
- package/dist/block_storage_vm.d.ts.map +0 -1
- package/dist/block_storage_vm.js +0 -258
- package/dist/block_storage_vm.js.map +0 -1
- package/dist/branding.d.ts +0 -7
- package/dist/branding.d.ts.map +0 -1
- package/dist/builder.cjs.map +0 -1
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js.map +0 -1
- package/dist/sdk_info.cjs +0 -10
- package/dist/sdk_info.cjs.map +0 -1
- package/dist/sdk_info.d.ts +0 -5
- package/dist/sdk_info.d.ts.map +0 -1
- package/dist/sdk_info.js +0 -8
- package/dist/sdk_info.js.map +0 -1
- package/dist/unionize.d.ts +0 -12
- package/dist/unionize.d.ts.map +0 -1
- package/src/block_storage_vm.ts +0 -346
- package/src/branding.ts +0 -4
- package/src/sdk_info.ts +0 -9
- package/src/unionize.ts +0 -12
package/src/filters/distill.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { DistributiveKeys, UnionToTuples } from "@milaboratories/helpers";
|
|
2
|
-
import {
|
|
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<
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
59
|
+
return distillLeaf(leaf as Record<string, unknown>) as R;
|
|
52
60
|
},
|
|
53
61
|
and: (results) => {
|
|
54
|
-
const filtered = results.filter((f): f is
|
|
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
|
|
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
|
}
|
package/src/filters/traverse.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
import type {
|
|
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<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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) =>
|
|
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) =>
|
|
59
|
+
.map((f) => traverseFilterSpecImpl(f, visitor)),
|
|
39
60
|
);
|
|
40
61
|
case "not":
|
|
41
|
-
return visitor.not(
|
|
62
|
+
return visitor.not(traverseFilterSpecImpl(filter.filter, visitor));
|
|
42
63
|
default:
|
|
43
|
-
return visitor.leaf(filter
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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:
|
|
56
|
-
if ("column" in leaf && leaf.column !== undefined) cols.push(leaf.column as
|
|
57
|
-
if ("rhs" in leaf && leaf.rhs !== undefined) cols.push(leaf.rhs as
|
|
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 "./
|
|
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 "./
|
|
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 "./
|
|
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,
|
|
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
|
-
|
|
70
|
+
UiStateOrData = unknown,
|
|
67
71
|
Href extends `/${string}` = `/${string}`,
|
|
68
72
|
> =
|
|
69
|
-
| PlatformaV1<Args, Outputs,
|
|
70
|
-
| PlatformaV2<Args, Outputs,
|
|
71
|
-
| PlatformaV3<Args, Outputs,
|
|
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
|
+
}
|