@milaboratories/pl-middle-layer 1.74.19 → 1.76.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 (87) hide show
  1. package/dist/index.cjs +1 -0
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.js +2 -2
  4. package/dist/middle_layer/block.cjs.map +1 -1
  5. package/dist/middle_layer/block.js.map +1 -1
  6. package/dist/middle_layer/block_action_context.cjs +57 -0
  7. package/dist/middle_layer/block_action_context.cjs.map +1 -0
  8. package/dist/middle_layer/block_action_context.d.ts +47 -0
  9. package/dist/middle_layer/block_action_context.d.ts.map +1 -0
  10. package/dist/middle_layer/block_action_context.js +57 -0
  11. package/dist/middle_layer/block_action_context.js.map +1 -0
  12. package/dist/middle_layer/build_stamp.cjs +1 -1
  13. package/dist/middle_layer/build_stamp.js +1 -1
  14. package/dist/middle_layer/column_links.cjs +106 -0
  15. package/dist/middle_layer/column_links.cjs.map +1 -0
  16. package/dist/middle_layer/column_links.js +104 -0
  17. package/dist/middle_layer/column_links.js.map +1 -0
  18. package/dist/middle_layer/index.d.ts +2 -1
  19. package/dist/middle_layer/middle_layer.cjs +1 -0
  20. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  21. package/dist/middle_layer/middle_layer.js +1 -0
  22. package/dist/middle_layer/middle_layer.js.map +1 -1
  23. package/dist/middle_layer/project.cjs +53 -19
  24. package/dist/middle_layer/project.cjs.map +1 -1
  25. package/dist/middle_layer/project.d.ts +6 -0
  26. package/dist/middle_layer/project.d.ts.map +1 -1
  27. package/dist/middle_layer/project.js +54 -20
  28. package/dist/middle_layer/project.js.map +1 -1
  29. package/dist/middle_layer/project_overview.cjs +56 -24
  30. package/dist/middle_layer/project_overview.cjs.map +1 -1
  31. package/dist/middle_layer/project_overview.js +55 -25
  32. package/dist/middle_layer/project_overview.js.map +1 -1
  33. package/dist/model/args_derivation.cjs +153 -0
  34. package/dist/model/args_derivation.cjs.map +1 -0
  35. package/dist/model/args_derivation.d.ts +21 -0
  36. package/dist/model/args_derivation.d.ts.map +1 -0
  37. package/dist/model/args_derivation.js +148 -0
  38. package/dist/model/args_derivation.js.map +1 -0
  39. package/dist/model/project_helper.cjs +34 -8
  40. package/dist/model/project_helper.cjs.map +1 -1
  41. package/dist/model/project_helper.d.ts +18 -5
  42. package/dist/model/project_helper.d.ts.map +1 -1
  43. package/dist/model/project_helper.js +34 -8
  44. package/dist/model/project_helper.js.map +1 -1
  45. package/dist/model/project_model.cjs +2 -1
  46. package/dist/model/project_model.cjs.map +1 -1
  47. package/dist/model/project_model.d.ts +1 -1
  48. package/dist/model/project_model.js +2 -1
  49. package/dist/model/project_model.js.map +1 -1
  50. package/dist/mutator/project.cjs +73 -12
  51. package/dist/mutator/project.cjs.map +1 -1
  52. package/dist/mutator/project.d.ts +1 -0
  53. package/dist/mutator/project.d.ts.map +1 -1
  54. package/dist/mutator/project.js +73 -12
  55. package/dist/mutator/project.js.map +1 -1
  56. package/dist/mutator/template/render_block.cjs +35 -2
  57. package/dist/mutator/template/render_block.cjs.map +1 -1
  58. package/dist/mutator/template/render_block.js +35 -4
  59. package/dist/mutator/template/render_block.js.map +1 -1
  60. package/dist/mutator/template/render_template.cjs +16 -1
  61. package/dist/mutator/template/render_template.cjs.map +1 -1
  62. package/dist/mutator/template/render_template.d.ts +12 -1
  63. package/dist/mutator/template/render_template.d.ts.map +1 -1
  64. package/dist/mutator/template/render_template.js +16 -2
  65. package/dist/mutator/template/render_template.js.map +1 -1
  66. package/package.json +16 -16
  67. package/src/middle_layer/block.ts +4 -1
  68. package/src/middle_layer/block_action_context.test.ts +148 -0
  69. package/src/middle_layer/block_action_context.ts +162 -0
  70. package/src/middle_layer/column_links.test.ts +350 -0
  71. package/src/middle_layer/column_links.ts +152 -0
  72. package/src/middle_layer/index.ts +6 -0
  73. package/src/middle_layer/middle_layer.ts +2 -0
  74. package/src/middle_layer/project.ts +52 -5
  75. package/src/middle_layer/project_overview.test.ts +200 -0
  76. package/src/middle_layer/project_overview.ts +82 -28
  77. package/src/model/args.test.ts +39 -1
  78. package/src/model/args_derivation.test.ts +326 -0
  79. package/src/model/args_derivation.ts +194 -0
  80. package/src/model/project_helper.ts +55 -25
  81. package/src/model/project_model.ts +7 -2
  82. package/src/mutator/project-v3.test.ts +105 -1
  83. package/src/mutator/project.ts +128 -37
  84. package/src/mutator/project_action_context.test.ts +178 -0
  85. package/src/mutator/template/render_block.ts +70 -5
  86. package/src/mutator/template/render_template.ts +22 -2
  87. package/src/test/fake_project.ts +222 -0
@@ -1,7 +1,7 @@
1
1
  import type { AnyRef, PlTransaction, ResourceRef, ResourceType } from "@milaboratories/pl-client";
2
2
  import { field, Pl } from "@milaboratories/pl-client";
3
3
  import { randomUUID } from "node:crypto";
4
- import { createRenderTemplate } from "./render_template";
4
+ import { createRenderTemplateWithResource } from "./render_template";
5
5
 
6
6
  export const BContextEnd: ResourceType = { name: "BContextEnd", version: "1" };
7
7
  export const BContext: ResourceType = { name: "BContext", version: "1" };
@@ -30,12 +30,63 @@ export type HeavyBlockOutputs = {
30
30
 
31
31
  export const HeavyBlockOutputNames: (keyof HeavyBlockOutputs)[] = ["context", "result"];
32
32
 
33
+ /** Baggage of the root action context of a block render. See the block action context spec. */
34
+ export type BlockActionContextBaggage = {
35
+ /** Display name of the block. */
36
+ name: string;
37
+ /** JSON of the block pack spec. */
38
+ blockPack: string;
39
+ blockId: string;
40
+ };
41
+
42
+ /** Resource type of a block action context. See ActionContextV1Type in the backend. */
43
+ export const ActionContextType: ResourceType = { name: "ActionContext", version: "1" };
44
+
45
+ /** The capability under which the backend accepts the "actionContext" link on a render. */
46
+ export const ActionContextLinkCapability = "optionalFieldsInSchemas:v1";
47
+
48
+ /**
49
+ * Creates the root action context of a block render: an ActionContext resource carrying the
50
+ * baggage, with a new random `run-id`, since each render is one run of the block and the contexts
51
+ * the templates create copy it. Undefined without baggage, or on a backend that cannot take the link.
52
+ */
53
+ function createBlockActionContext(
54
+ tx: PlTransaction,
55
+ baggage: BlockActionContextBaggage | undefined,
56
+ ): ResourceRef | undefined {
57
+ if (baggage === undefined || !tx.hasCapability(ActionContextLinkCapability)) return undefined;
58
+ return tx.createEphemeral(
59
+ ActionContextType,
60
+ JSON.stringify({
61
+ name: baggage.name,
62
+ "block-pack": baggage.blockPack,
63
+ "block-id": baggage.blockId,
64
+ "run-id": randomUUID(),
65
+ }),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Creates the heavy block render, and with `baggage` its root action context in the same
71
+ * transaction. Returns `actionContext`: the render is garbage collected after its outputs resolve,
72
+ * so the caller must hold the context to keep it.
73
+ */
33
74
  export function createRenderHeavyBlock(
34
75
  tx: PlTransaction,
35
76
  tpl: AnyRef,
36
77
  inputs: HeavyBlockInputs,
37
- ): HeavyBlockOutputs {
38
- return createRenderTemplate(tx, tpl, true, inputs, HeavyBlockOutputNames);
78
+ baggage?: BlockActionContextBaggage,
79
+ ): HeavyBlockOutputs & { actionContext?: ResourceRef } {
80
+ const actionContext = createBlockActionContext(tx, baggage);
81
+ const { outputs } = createRenderTemplateWithResource(
82
+ tx,
83
+ tpl,
84
+ true,
85
+ inputs,
86
+ HeavyBlockOutputNames,
87
+ actionContext,
88
+ );
89
+ return actionContext === undefined ? outputs : { ...outputs, actionContext };
39
90
  }
40
91
 
41
92
  export type LightBlockInputs = {
@@ -51,12 +102,26 @@ export type LightBlockOutput = {
51
102
 
52
103
  export const LightBlockOutputNames: (keyof LightBlockOutput)[] = ["result"];
53
104
 
105
+ /**
106
+ * Creates the light block render, with the same action context as a heavy one, so template code
107
+ * of either kind knows its block and run.
108
+ */
54
109
  export function createRenderLightBlock(
55
110
  tx: PlTransaction,
56
111
  tpl: ResourceRef,
57
112
  inputs: LightBlockInputs,
58
- ): LightBlockOutput {
59
- return createRenderTemplate(tx, tpl, true, inputs, LightBlockOutputNames);
113
+ baggage?: BlockActionContextBaggage,
114
+ ): LightBlockOutput & { actionContext?: ResourceRef } {
115
+ const actionContext = createBlockActionContext(tx, baggage);
116
+ const { outputs } = createRenderTemplateWithResource(
117
+ tx,
118
+ tpl,
119
+ true,
120
+ inputs,
121
+ LightBlockOutputNames,
122
+ actionContext,
123
+ );
124
+ return actionContext === undefined ? outputs : { ...outputs, actionContext };
60
125
  }
61
126
 
62
127
  export function createBContextEnd(tx: PlTransaction): ResourceRef {
@@ -1,4 +1,4 @@
1
- import type { AnyRef, FieldRef, PlTransaction } from "@milaboratories/pl-client";
1
+ import type { AnyRef, FieldRef, PlTransaction, ResourceRef } from "@milaboratories/pl-client";
2
2
  import { field, Pl } from "@milaboratories/pl-client";
3
3
  import type { ResourceType } from "@platforma-sdk/model";
4
4
 
@@ -27,6 +27,24 @@ export function createRenderTemplate<O extends string>(
27
27
  inputs: Pl.PlRecord,
28
28
  outputNames: O[],
29
29
  ): Record<O, FieldRef> {
30
+ return createRenderTemplateWithResource(tx, tpl, ephemeral, inputs, outputNames).outputs;
31
+ }
32
+
33
+ /**
34
+ * Same as {@link createRenderTemplate}, and also returns the render resource itself.
35
+ *
36
+ * With `actionContext`, the render's "actionContext" service field points to it. The link is created
37
+ * before the inputs are locked: the backend accepts it only in the creating transaction, ahead of
38
+ * any explicit input lock.
39
+ */
40
+ export function createRenderTemplateWithResource<O extends string>(
41
+ tx: PlTransaction,
42
+ tpl: AnyRef,
43
+ ephemeral: boolean,
44
+ inputs: Pl.PlRecord,
45
+ outputNames: O[],
46
+ actionContext?: AnyRef,
47
+ ): { render: ResourceRef; outputs: Record<O, FieldRef> } {
30
48
  if (outputNames.length === 0) throw new Error("Zero output names provided");
31
49
  const rId = ephemeral ? tx.createEphemeral(EphRenderTemplate) : tx.createStruct(RenderTemplate);
32
50
 
@@ -35,7 +53,9 @@ export function createRenderTemplate<O extends string>(
35
53
 
36
54
  tx.createField(tplField, "Input", tpl);
37
55
  tx.createField(inputsField, "Input", Pl.createPlMap(tx, inputs, ephemeral));
56
+ if (actionContext !== undefined)
57
+ tx.createField(field(rId, "actionContext"), "Service", actionContext);
38
58
  tx.lockInputs(rId);
39
59
 
40
- return Pl.futureRecord(tx, rId, outputNames, "Output", "outputs/");
60
+ return { render: rId, outputs: Pl.futureRecord(tx, rId, outputNames, "Output", "outputs/") };
41
61
  }
@@ -0,0 +1,222 @@
1
+ import {
2
+ createSignedResourceId,
3
+ DefaultFinalResourceDataPredicate,
4
+ NullSignedResourceId,
5
+ type FieldData,
6
+ type FieldType,
7
+ type SignedResourceId,
8
+ } from "@milaboratories/pl-client";
9
+ import type { PColumnSpec, ArgsIssue } from "@milaboratories/pl-model-common";
10
+ import { PlTreeState, type ExtendedResourceData } from "@milaboratories/pl-tree";
11
+ import { argsFingerprint, type ArgsErrorsRecord } from "../model/args_derivation";
12
+ import {
13
+ projectFieldName,
14
+ ProjectStructureKey,
15
+ type ProjectField,
16
+ type ProjectStructure,
17
+ } from "../model/project_model";
18
+
19
+ /** What a project field holds: a resource, an error, or nothing yet. */
20
+ export type FieldContent = SignedResourceId | { error: SignedResourceId } | "unresolved";
21
+
22
+ /** Columns of a result list by name: the spec of each and the cells of its data. */
23
+ export type Columns = Record<string, { spec: PColumnSpec; cells: string[] }>;
24
+
25
+ /**
26
+ * State of a result list: `locked` — complete; `open` — more columns may come; `failed` — it has
27
+ * an error and will never lock.
28
+ */
29
+ export type FrameState = { state: "locked" | "open" | "failed" };
30
+
31
+ /** The block fields a {@link FakeProject} holds. */
32
+ export type FakeProjectField = Extract<
33
+ ProjectField["fieldName"],
34
+ | "currentArgs"
35
+ | "inputsValid"
36
+ | "prodArgs"
37
+ | "prodCtx"
38
+ | "prodUiCtx"
39
+ | "stagingCtx"
40
+ | "stagingUiCtx"
41
+ >;
42
+
43
+ /** A project resource tree built in memory: its blocks, in project order, and their fields. */
44
+ export class FakeProject {
45
+ private readonly nodes: ExtendedResourceData[] = [];
46
+ private sent = 0;
47
+ private readonly blocks = new Map<string, Partial<Record<FakeProjectField, FieldContent>>>();
48
+ private readonly frameFields = new Map<SignedResourceId, FieldData[]>();
49
+
50
+ /** Adds a block below the others, or replaces the fields of one already added. */
51
+ block(blockId: string, fields: Partial<Record<FakeProjectField, FieldContent>>): void {
52
+ this.blocks.set(blockId, fields);
53
+ }
54
+
55
+ /** Args of a block. */
56
+ args(args: unknown): SignedResourceId {
57
+ return this.value("json/object", args);
58
+ }
59
+
60
+ /** Args errors of a block found in the args `forArgs`, as its `inputsValid` field holds them. */
61
+ argsErrors(errors: readonly ArgsIssue[], forArgs: unknown): SignedResourceId {
62
+ const record: ArgsErrorsRecord = { argsFingerprint: argsFingerprint(forArgs), errors };
63
+ return this.value("json/object", record);
64
+ }
65
+
66
+ /** A ready value resource holding `json`. */
67
+ value(type: string, json: unknown): SignedResourceId {
68
+ const id = this.newId();
69
+ this.nodes.push({
70
+ ...READY,
71
+ id,
72
+ kind: "Value",
73
+ type: { name: type, version: "1" },
74
+ data: Buffer.from(JSON.stringify(json)),
75
+ fields: [],
76
+ });
77
+ return id;
78
+ }
79
+
80
+ /** An error a resource or field can hold. */
81
+ error(message: string): SignedResourceId {
82
+ return this.value("json/resourceError", { message });
83
+ }
84
+
85
+ /** A result list exporting the given columns, each with JSON data holding `cells`. */
86
+ frame(columns: Columns, state: FrameState = { state: "locked" }): SignedResourceId {
87
+ const id = this.newId();
88
+ this.frameFields.set(id, []);
89
+ this.extend(id, columns, state);
90
+ return id;
91
+ }
92
+
93
+ /** Adds columns to a result list made by {@link frame}, and moves it to `state`. */
94
+ extend(id: SignedResourceId, columns: Columns, { state }: FrameState): void {
95
+ const fields = this.frameFields.get(id);
96
+ if (fields === undefined) throw new Error("Not a result list of this project");
97
+ for (const [name, { spec, cells }] of Object.entries(columns)) {
98
+ const data = Object.fromEntries(cells.map((cell, i) => [`["s${i}"]`, cell]));
99
+ fields.push(field("Input", `${name}.spec`, this.value("PColumnSpec", spec)));
100
+ fields.push(
101
+ field("Input", `${name}.data`, this.value("PColumnData/Json", { keyLength: 1, data })),
102
+ );
103
+ }
104
+ const error = state === "failed" ? this.error("render failed") : NullSignedResourceId;
105
+ this.nodes.push({
106
+ ...READY,
107
+ id,
108
+ kind: "Structural",
109
+ type: { name: "PFrame", version: "1" },
110
+ inputsLocked: state === "locked",
111
+ resourceReady: state === "locked",
112
+ final: state === "locked",
113
+ error,
114
+ fields: [...fields],
115
+ });
116
+ }
117
+
118
+ /** The tree of the project as it is now. */
119
+ tree(): PlTreeState {
120
+ const tree = new PlTreeState(ROOT_ID, DefaultFinalResourceDataPredicate);
121
+ tree.updateFromResourceData(this.changes());
122
+ return tree;
123
+ }
124
+
125
+ /**
126
+ * The project resource and the resources added or changed since the last call: a final
127
+ * resource already in the tree is not sent again.
128
+ */
129
+ changes(): ExtendedResourceData[] {
130
+ const structure: ProjectStructure = {
131
+ groups: [
132
+ {
133
+ id: "main",
134
+ label: "Main",
135
+ blocks: [...this.blocks.keys()].map((id) => ({ id, label: id, renderingMode: "Heavy" })),
136
+ },
137
+ ],
138
+ };
139
+ const fields = [...this.blocks].flatMap(([blockId, contents]) =>
140
+ PROJECT_FIELDS.flatMap((name) => {
141
+ const content = contents[name];
142
+ return content === undefined
143
+ ? []
144
+ : [projectField(projectFieldName(blockId, name), content)];
145
+ }),
146
+ );
147
+ const root: ExtendedResourceData = {
148
+ ...READY,
149
+ final: false,
150
+ id: ROOT_ID,
151
+ kind: "Structural",
152
+ type: { name: "UserProject", version: "2" },
153
+ fields,
154
+ kv: [{ key: ProjectStructureKey, value: Buffer.from(JSON.stringify(structure)) }],
155
+ };
156
+ const added = this.nodes.slice(this.sent);
157
+ this.sent = this.nodes.length;
158
+ return [root, ...added];
159
+ }
160
+
161
+ private newId(): SignedResourceId {
162
+ return createSignedResourceId(nextResourceId++);
163
+ }
164
+ }
165
+
166
+ // Internals
167
+
168
+ const ROOT_ID = createSignedResourceId(1_000_001n);
169
+
170
+ /**
171
+ * Resource ids stay unique across tests, as they are on a backend: the middle layer caches the
172
+ * columns an args resource references by its id.
173
+ */
174
+ let nextResourceId = 100n;
175
+
176
+ const READY: Omit<ExtendedResourceData, "id" | "kind" | "type" | "fields"> = {
177
+ originalResourceId: NullSignedResourceId,
178
+ error: NullSignedResourceId,
179
+ inputsLocked: true,
180
+ outputsLocked: true,
181
+ resourceReady: true,
182
+ final: true,
183
+ kv: [],
184
+ };
185
+
186
+ const PROJECT_FIELDS: readonly FakeProjectField[] = [
187
+ "currentArgs",
188
+ "inputsValid",
189
+ "prodArgs",
190
+ "prodCtx",
191
+ "prodUiCtx",
192
+ "stagingCtx",
193
+ "stagingUiCtx",
194
+ ];
195
+
196
+ function field(type: FieldType, name: string, value: SignedResourceId): FieldData {
197
+ return { name, type, value, error: NullSignedResourceId, status: "Resolved", valueIsFinal: true };
198
+ }
199
+
200
+ function projectField(name: string, content: FieldContent): FieldData {
201
+ if (content === "unresolved") {
202
+ return {
203
+ name,
204
+ type: "Dynamic",
205
+ value: NullSignedResourceId,
206
+ error: NullSignedResourceId,
207
+ status: "Empty",
208
+ valueIsFinal: false,
209
+ };
210
+ }
211
+ if (typeof content === "object" && "error" in content) {
212
+ return {
213
+ name,
214
+ type: "Dynamic",
215
+ value: NullSignedResourceId,
216
+ error: content.error,
217
+ status: "Resolved",
218
+ valueIsFinal: true,
219
+ };
220
+ }
221
+ return field("Dynamic", name, content);
222
+ }