@platforma-sdk/workflow-tengo 2.12.1 → 2.13.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 (29) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +3 -3
  3. package/dist/index.mjs +3 -3
  4. package/dist/tengo/lib/pframes.builder.lib.tengo +35 -0
  5. package/dist/tengo/lib/pframes.constants.lib.tengo +14 -0
  6. package/dist/tengo/lib/pframes.lib.tengo +488 -22
  7. package/dist/tengo/lib/pframes.ll.lib.tengo +61 -47
  8. package/dist/tengo/lib/pframes.spec.lib.tengo +135 -0
  9. package/dist/tengo/lib/pframes.util.lib.tengo +37 -1
  10. package/dist/tengo/lib/pframes.xsv.lib.tengo +2 -2
  11. package/dist/tengo/lib/slices.lib.tengo +16 -1
  12. package/dist/tengo/lib/validation.lib.tengo +1 -1
  13. package/dist/tengo/tpl/exec.R.create-renv.plj.gz +0 -0
  14. package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
  15. package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
  16. package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
  17. package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
  18. package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
  19. package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
  20. package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
  21. package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
  22. package/dist/tengo/tpl/pframes.map-pframe.plj.gz +0 -0
  23. package/dist/tengo/tpl/pframes.process-pcolumn-data.plj.gz +0 -0
  24. package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
  25. package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
  26. package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
  27. package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
  28. package/package.json +1 -1
  29. package/dist/tengo/tpl/pframes.aggregate.plj.gz +0 -0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  declare type TemplateFromFile = { readonly type: "from-file"; readonly path: string; };
2
- declare type TplName = "exec.monetization_to_json" | "pframes.aggregate" | "pframes.export-single-pcolumn" | "pframes.import-dir" | "pframes.map-pframe" | "workdir.save" | "workflow.build-ctx" | "exec.R.create-renv" | "exec.python.install-deps" | "pframes.export-multiple-pcolumns" | "pframes.export-pframe" | "exec.python.create-venv" | "exec.exec" | "pframes.xsv-export-pframe" | "pframes.xsv-import-map";
2
+ declare type TplName = "exec.monetization_to_json" | "pframes.export-single-pcolumn" | "pframes.import-dir" | "pframes.map-pframe" | "workdir.save" | "workflow.build-ctx" | "exec.R.create-renv" | "exec.python.install-deps" | "pframes.export-multiple-pcolumns" | "exec.python.create-venv" | "exec.exec" | "pframes.xsv-export-pframe" | "pframes.xsv-import-map" | "pframes.process-pcolumn-data" | "pframes.export-pframe";
3
3
  declare const Templates: Record<TplName, TemplateFromFile>;
4
4
  export { Templates };
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  module.exports = { Templates: {
2
2
  'exec.monetization_to_json': { type: 'from-file', path: require.resolve('./tengo/tpl/exec.monetization_to_json.plj.gz') },
3
- 'pframes.aggregate': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.aggregate.plj.gz') },
4
3
  'pframes.export-single-pcolumn': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.export-single-pcolumn.plj.gz') },
5
4
  'pframes.import-dir': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.import-dir.plj.gz') },
6
5
  'pframes.map-pframe': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.map-pframe.plj.gz') },
@@ -9,9 +8,10 @@ module.exports = { Templates: {
9
8
  'exec.R.create-renv': { type: 'from-file', path: require.resolve('./tengo/tpl/exec.R.create-renv.plj.gz') },
10
9
  'exec.python.install-deps': { type: 'from-file', path: require.resolve('./tengo/tpl/exec.python.install-deps.plj.gz') },
11
10
  'pframes.export-multiple-pcolumns': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.export-multiple-pcolumns.plj.gz') },
12
- 'pframes.export-pframe': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.export-pframe.plj.gz') },
13
11
  'exec.python.create-venv': { type: 'from-file', path: require.resolve('./tengo/tpl/exec.python.create-venv.plj.gz') },
14
12
  'exec.exec': { type: 'from-file', path: require.resolve('./tengo/tpl/exec.exec.plj.gz') },
15
13
  'pframes.xsv-export-pframe': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.xsv-export-pframe.plj.gz') },
16
- 'pframes.xsv-import-map': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.xsv-import-map.plj.gz') }
14
+ 'pframes.xsv-import-map': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.xsv-import-map.plj.gz') },
15
+ 'pframes.process-pcolumn-data': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.process-pcolumn-data.plj.gz') },
16
+ 'pframes.export-pframe': { type: 'from-file', path: require.resolve('./tengo/tpl/pframes.export-pframe.plj.gz') }
17
17
  }};
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  import { resolve } from 'node:path';
2
2
  export const Templates = {
3
3
  'exec.monetization_to_json': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/exec.monetization_to_json.plj.gz') },
4
- 'pframes.aggregate': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.aggregate.plj.gz') },
5
4
  'pframes.export-single-pcolumn': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.export-single-pcolumn.plj.gz') },
6
5
  'pframes.import-dir': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.import-dir.plj.gz') },
7
6
  'pframes.map-pframe': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.map-pframe.plj.gz') },
@@ -10,9 +9,10 @@ export const Templates = {
10
9
  'exec.R.create-renv': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/exec.R.create-renv.plj.gz') },
11
10
  'exec.python.install-deps': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/exec.python.install-deps.plj.gz') },
12
11
  'pframes.export-multiple-pcolumns': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.export-multiple-pcolumns.plj.gz') },
13
- 'pframes.export-pframe': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.export-pframe.plj.gz') },
14
12
  'exec.python.create-venv': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/exec.python.create-venv.plj.gz') },
15
13
  'exec.exec': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/exec.exec.plj.gz') },
16
14
  'pframes.xsv-export-pframe': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.xsv-export-pframe.plj.gz') },
17
- 'pframes.xsv-import-map': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.xsv-import-map.plj.gz') }
15
+ 'pframes.xsv-import-map': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.xsv-import-map.plj.gz') },
16
+ 'pframes.process-pcolumn-data': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.process-pcolumn-data.plj.gz') },
17
+ 'pframes.export-pframe': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/pframes.export-pframe.plj.gz') }
18
18
  };
@@ -0,0 +1,35 @@
1
+ ll := import("@platforma-sdk/workflow-tengo:ll")
2
+ oop := import("@platforma-sdk/workflow-tengo:oop")
3
+ smart := import("@platforma-sdk/workflow-tengo:smart")
4
+ pConstants := import("@platforma-sdk/workflow-tengo:pframes.constants")
5
+
6
+
7
+ /**
8
+ * Creates a builder for p-frame resource.
9
+ */
10
+ pFrameBuilder := func() {
11
+ r := smart.structBuilder(pConstants.RTYPE_P_FRAME)
12
+
13
+ self := undefined
14
+ self = ll.toStrict(oop.inherit(r, {
15
+
16
+ add: func(column, spec, data) {
17
+ if (!is_undefined(spec)) {
18
+ r.createInputField(column + ".spec").setRefOrJson(spec)
19
+ }
20
+ if (!is_undefined(data)) {
21
+ r.createInputField(column + ".data").setRefOrJson(data)
22
+ }
23
+ return self
24
+ },
25
+
26
+ build: func() {
27
+ return r.lockAndBuild()
28
+ }
29
+ }))
30
+ return self
31
+ }
32
+
33
+ export ll.toStrict({
34
+ pFrameBuilder: pFrameBuilder
35
+ })
@@ -17,6 +17,18 @@ VALUE_FIELD_NAME := "__value__"
17
17
  */
18
18
  KEY_FIELD_NAME := "__key__"
19
19
 
20
+ /**
21
+ * Name of the field under which the aggregation axes specification is passed
22
+ * to the iteration body template.
23
+ */
24
+ AGGREGATION_AXES_SPEC_FIELD_NAME := "__aggregation_axes_spec__"
25
+
26
+ /**
27
+ * Name of the field under which the aggregation axes names are passed
28
+ * to the iteration body template.
29
+ */
30
+ AGGREGATION_AXES_NAMES_FIELD_NAME := "__aggregation_axes_names__"
31
+
20
32
  RTYPE_P_FRAME := { Name: "PFrame", Version: "1" }
21
33
 
22
34
  P_COLUMN_DATA_TYPE_PREFIX := "PColumnData/"
@@ -37,6 +49,8 @@ export ll.toStrict({
37
49
  KIND_P_COLUMN : KIND_P_COLUMN,
38
50
  KEY_FIELD_NAME : KEY_FIELD_NAME,
39
51
  VALUE_FIELD_NAME : VALUE_FIELD_NAME,
52
+ AGGREGATION_AXES_SPEC_FIELD_NAME : AGGREGATION_AXES_SPEC_FIELD_NAME,
53
+ AGGREGATION_AXES_NAMES_FIELD_NAME : AGGREGATION_AXES_NAMES_FIELD_NAME,
40
54
  P_COLUMN_DATA_TYPE_PREFIX : P_COLUMN_DATA_TYPE_PREFIX,
41
55
  P_COLUMN_SUPER_PARTITIONED_DATA_TYPE_PREFIX : P_COLUMN_SUPER_PARTITIONED_DATA_TYPE_PREFIX,
42
56
 
@@ -3,12 +3,19 @@
3
3
  */
4
4
 
5
5
  ll := import("@platforma-sdk/workflow-tengo:ll")
6
- oop := import("@platforma-sdk/workflow-tengo:oop")
6
+ maps := import("@platforma-sdk/workflow-tengo:maps")
7
7
  smart := import("@platforma-sdk/workflow-tengo:smart")
8
+ slices := import("@platforma-sdk/workflow-tengo:slices")
8
9
  assets := import("@platforma-sdk/workflow-tengo:assets")
9
10
  render := import("@platforma-sdk/workflow-tengo:render")
11
+ builder := import("@platforma-sdk/workflow-tengo:pframes.builder")
12
+ pUtil := import("@platforma-sdk/workflow-tengo:pframes.util")
13
+ pSpec := import("@platforma-sdk/workflow-tengo:pframes.spec")
14
+ validation := import("@platforma-sdk/workflow-tengo:validation")
10
15
  constants := import("@platforma-sdk/workflow-tengo:pframes.constants")
11
16
 
17
+ llProcessTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:pframes.process-pcolumn-data")
18
+
12
19
  /**
13
20
  * Given a reference to the p-frame, converts all inner resource files into resources that can be
14
21
  * downloaded from the UI side.
@@ -25,41 +32,500 @@ exportColumnData := func(data) {
25
32
  }
26
33
 
27
34
  /**
28
- * Creates a builder for p-frame resource.
35
+ * Creates a builder for a resource map p-column's data.
29
36
  */
30
- pFrameBuilder := func() {
31
- r := smart.structBuilder(constants.RTYPE_P_FRAME)
37
+ pColumnResourceMapDataBuilder := func(keyLength) {
38
+ ll.panic("pframes.pColumnResourceMapDataBuilder is not supported any more; use pcolumn.resourceMapBuilder")
39
+ }
40
+
41
+ /**
42
+ * Constructs a mapping or aggregation pipeline for a given input PColumn and returns a specialized
43
+ * output object for accessing the processing results.
44
+ *
45
+ * === Body Template Contract ===
46
+ *
47
+ * Inputs:
48
+ * "__key__" - When passKey is true, contains the JSON representation of the group/element key:
49
+ * - Without aggregation: represents a single key
50
+ * - With aggregation: represents all keys in positions not specified by indices
51
+ * Not provided if passKey is false or omitted from ops
52
+ * "__aggregation_axes_spec__"
53
+ * - When passAggregationAxesSpec is true, contains the axes specification for aggregation
54
+ * Can be used to adapt body behavior when optional aggregation axes are unmatched
55
+ * Not provided if passAggregationAxesSpec is false or omitted from ops
56
+ * "__aggregation_axes_names__"
57
+ * - When passAggregationAxesNames is true, contains just the names of aggregation axes
58
+ * as a simple array of strings, without domains, annotations, etc.
59
+ * Not provided if passAggregationAxesNames is false or omitted from ops
60
+ * "__value__" - Content depends on aggregation setting:
61
+ * - Without aggregation: single value (only for ResourceMap PColumns)
62
+ * - With aggregation: "PColumnData/*" resource containing all records in current group
63
+ * ...extra - Additional values/references passed via the options.extra parameter
64
+ * are forwarded to the template unchanged
65
+ *
66
+ * === Parameters ===
67
+ *
68
+ * @param input Primary input column data and specifications
69
+ *
70
+ * Required structure:
71
+ * {
72
+ * // PColumn specifications (must be explicit, not a reference)
73
+ * spec: <PColumnSpec>,
74
+ *
75
+ * // Reference to the data resource
76
+ * data: <resource ref>
77
+ * }
78
+ *
79
+ * @param bodyTpl Template executed for each iteration (see contract above)
80
+ * @param outputs Array of output configurations to collect from rendered templates
81
+ *
82
+ * Common fields for all types:
83
+ * {
84
+ * name: string // Output name
85
+ * path?: string[] // [Optional] Override path inside body template result
86
+ * First element interpreted as output name
87
+ * Defaults to [name] if omitted
88
+ * }
89
+ *
90
+ * Available types:
91
+ * | {
92
+ * type: "Resource", // Single resource output
93
+ * spec: PColumnSpec // Result column spec (must have zero axes specs,
94
+ * // input axes specs added automatically)
95
+ * }
96
+ * | {
97
+ * type: "ResourceMap", // ResourceMap output
98
+ * spec: PColumnSpec // Result column spec (list only axes specs
99
+ * // produced by body template, input axes added automatically)
100
+ * }
101
+ * | {
102
+ * type: "BinaryPartitioned" | "JsonPartitioned",
103
+ * // Partitioned Json/Binary PColumn data
104
+ * spec: PColumnSpec // Result column spec (list only axes specs
105
+ * // produced by body template, input axes added automatically)
106
+ * partitionKeyLength: number
107
+ * // Expected partition length per operation
108
+ * }
109
+ * | {
110
+ * type: "Xsv", // CSV/TSV formatted text file for xsv conversion
111
+ * xsvType: "tsv"|"csv", // Expected file format
112
+ * settings: object // Xsv conversion specification
113
+ * }
114
+ *
115
+ * @param opts Optional additional settings
116
+ *
117
+ * {
118
+ * traceSteps?: array // Array of trace steps to add to output specs
119
+ * // Each step should be an object with type, id, importance and label
120
+ *
121
+ * aggregate?: AxisMatcher[],
122
+ * // If specified, selected axes are combined and
123
+ * // passed to each iteration as a group. These axes
124
+ * // won't be automatically added to output specs
125
+ * // Examples:
126
+ * // - "pl7.app/sequencing/readIndex"
127
+ * // - { name: "pl7.app/sequencing/readIndex" }
128
+ * // - { type: "String", name: "pl7.app/sequencing/lane" }
129
+ * // - { type: "String", name: "pl7.app/sequencing/lane", optional: true }
130
+ * // - { type: "String", name: "pl7.app/rna-seq/geneId", domain: { "pl7.app/species": "homo-sapiens" } }
131
+ * // With optional: true, matcher won't error if unmatched
132
+ * // Use passAggregationAxesSpec for custom handling in body template
133
+ *
134
+ * passKey?: boolean // Pass __key__ to body template if true
135
+ *
136
+ * passAggregationAxesSpec?: boolean
137
+ * // Pass full aggregation axes specs to body template
138
+ * // via __aggregation_axes_spec__ input
139
+ *
140
+ * passAggregationAxesNames?: boolean
141
+ * // Pass array of aggregation axes names to body template
142
+ * // via __aggregation_axes_names__ input
143
+ *
144
+ * extra?: Map<string, Ref>,
145
+ * // Additional parameters passed to each body invocation
146
+ * // Keys match template input names
147
+ *
148
+ * isEphemeral?: boolean // Whether to render mapping template in ephemeral mode
149
+ * // Defaults to false if omitted, template will be rendered
150
+ * // as pure resource
151
+ * }
152
+ *
153
+ * === Return ===
154
+ *
155
+ * @returns Object with methods for retrieving processing results
156
+ * See source code for method documentation and signatures
157
+ */
158
+ processColumn := func(input, bodyTpl, outputs, ...opts) {
159
+ if len(opts) == 0 {
160
+ opts = {}
161
+ } else if len(opts) == 1 {
162
+ opts = opts[0]
163
+ } else {
164
+ ll.panic("too many arguments")
165
+ }
166
+
167
+ if is_undefined(input.spec) {
168
+ ll.panic("no input spec provided")
169
+ }
170
+
171
+ if !smart.isResource(input.data) {
172
+ ll.panic("input data should be a resource")
173
+ }
174
+
175
+ validation.assertType(input.spec, pSpec.P_COLUMN_SPEC_SCHEMA)
176
+
177
+ if is_undefined(opts.passKey) {
178
+ opts.passKey = false
179
+ }
180
+
181
+ if is_undefined(opts.passAggregationAxesSpec) {
182
+ opts.passAggregationAxesSpec = false
183
+ }
184
+
185
+ if is_undefined(opts.passAggregationAxesNames) {
186
+ opts.passAggregationAxesNames = false
187
+ }
188
+
189
+ if is_undefined(opts.isEphemeral) {
190
+ opts.isEphemeral = false
191
+ }
192
+
193
+ if (opts.passAggregationAxesSpec || opts.passAggregationAxesNames) && is_undefined(opts.aggregate) {
194
+ ll.panic("can't use passAggregationAxesSpec or passAggregationAxesNames without aggregate")
195
+ }
196
+
197
+ // Initialize trace if traceSteps provided
198
+ trace := undefined
199
+ if !is_undefined(opts.traceSteps) {
200
+ trace = pSpec.makeTrace(input.spec, opts.traceSteps...)
201
+ }
202
+
203
+ // JSON params passed as a single resource to the template
204
+ processTemplateParams := {
205
+ eph: opts.isEphemeral,
206
+ passKey: opts.passKey
207
+ }
208
+
209
+ extra := opts.extra
210
+ if is_undefined(extra) {
211
+ extra = {}
212
+ }
213
+
214
+ iterationAxesSpec := input.spec.axesSpec
215
+ processTemplateParams.expectedKeyLength = len(input.spec.axesSpec)
216
+ if !is_undefined(opts.aggregate) {
217
+ aggregationIndices := pSpec.matchAxes(input.spec.axesSpec, opts.aggregate)
218
+ processTemplateParams.aggregationIndices = aggregationIndices
219
+
220
+ // calculate iteration axes spec
221
+ groupAxesIndices := pUtil.calculateGroupAxesIndices(len(input.spec.axesSpec), aggregationIndices)
222
+ iterationAxesSpec = []
223
+ for i in groupAxesIndices {
224
+ iterationAxesSpec = append(iterationAxesSpec, input.spec.axesSpec[i])
225
+ }
226
+
227
+ if opts.passAggregationAxesSpec {
228
+ aggregationAxesSpec := []
229
+ for i in aggregationIndices {
230
+ aggregationAxesSpec = append(aggregationAxesSpec, input.spec.axesSpec[i])
231
+ }
232
+
233
+ extra[constants.AGGREGATION_AXES_SPEC_FIELD_NAME] = smart.createJsonResource(aggregationAxesSpec)
234
+ }
235
+
236
+ if opts.passAggregationAxesNames {
237
+ aggregationAxesNames := []
238
+ for i in aggregationIndices {
239
+ aggregationAxesNames = append(aggregationAxesNames, input.spec.axesSpec[i].name)
240
+ }
241
+
242
+ extra[constants.AGGREGATION_AXES_NAMES_FIELD_NAME] = smart.createJsonResource(aggregationAxesNames)
243
+ }
244
+ }
245
+
246
+ processedOutputs := []
247
+ outputSpecs := {}
248
+ outputsMap := {}
249
+ for output in outputs {
250
+ // map output name to output settings provided in input
251
+ outputsMap[output.name] = output
252
+ if output.type == "Resource" || output.type == "BinaryPartitioned" || output.type == "JsonPartitioned" {
253
+ spec := maps.merge(output.spec, { axesSpec: iterationAxesSpec + slices.normalize(output.spec.axesSpec) })
254
+ if !is_undefined(trace) {
255
+ spec = trace.inject(spec)
256
+ }
257
+ outputSpecs[output.name] = spec
258
+ processedOutputs = append(processedOutputs, maps.merge(output, {spec: undefined })) // removing spec field
259
+ } else if output.type == "ResourceMap" {
260
+ processedOutputs = append(processedOutputs, maps.merge(output,
261
+ { keyLength: len(output.spec.axesSpec), spec: undefined }))
262
+ spec := maps.merge(output.spec, { axesSpec: iterationAxesSpec + slices.normalize(output.spec.axesSpec) })
263
+ if !is_undefined(trace) {
264
+ spec = trace.inject(spec)
265
+ }
266
+ outputSpecs[output.name] = spec
267
+ } else if output.type == "Xsv" {
268
+ decomposition := pUtil.decomposePfconvImportCfg(output.settings, {
269
+ additionalAxesSpec: iterationAxesSpec
270
+ })
271
+ purifiedPfconvCfg := decomposition.purifiedCfg
272
+ columnsSpec := decomposition.columnsSpec
273
+ if !is_undefined(trace) {
274
+ for columnName, spec in columnsSpec {
275
+ columnsSpec[columnName] = trace.inject(spec)
276
+ }
277
+ }
278
+ outputSpecs[output.name] = columnsSpec
279
+ processedOutputs = append(processedOutputs, maps.merge(output,
280
+ {
281
+ settings: purifiedPfconvCfg,
282
+ flattenWithDelimiter: "/"
283
+ }))
284
+ } else {
285
+ ll.panic("unknown output type: " + output.type)
286
+ }
287
+ }
288
+ processTemplateParams.outputs = processedOutputs
289
+
290
+ renderInputs := {
291
+ params: smart.createJsonResource(processTemplateParams),
292
+ body: bodyTpl,
293
+ data: input.data
294
+ }
295
+
296
+ for k, v in extra {
297
+ renderInputs["__extra_" + k] = v
298
+ }
299
+
300
+ renderResult := render.createEphemeral(llProcessTpl, renderInputs)
32
301
 
33
302
  self := undefined
34
- self = ll.toStrict(oop.inherit(r, {
303
+ self = ll.toStrict({
304
+ renderResult: renderResult,
305
+
306
+ /**
307
+ * Returns output spec for the given output name and optional column name
308
+ *
309
+ * For normal outputs (Resource, ResourceMap, BinaryPartitioned, JsonPartitioned),
310
+ * only the output name should be provided.
311
+ * For Xsv outputs, both output name and column name should be provided.
312
+ *
313
+ * @param name - Name of the output
314
+ * @param column - Optional. For Xsv outputs, name of the column
315
+ * @returns Output spec
316
+ */
317
+ outputSpec: func(name, ...column) {
318
+ output := outputsMap[name]
319
+ if is_undefined(output) {
320
+ ll.panic("unknown output: " + name)
321
+ }
322
+
323
+ spec := outputSpecs[name]
324
+ if output.type == "Xsv" {
325
+ if len(column) != 1 {
326
+ ll.panic("column name must be provided for Xsv output")
327
+ }
328
+ columnName := column[0]
329
+ spec = spec[columnName]
330
+ if is_undefined(spec) {
331
+ ll.panic("unknown column: " + columnName)
332
+ }
333
+ }
334
+
335
+ if len(column) > 0 {
336
+ ll.panic("column name should not be provided for non-Xsv output")
337
+ }
338
+ return spec
339
+ },
340
+
341
+ /**
342
+ * Returns data resource for the given output name and optional column name
343
+ *
344
+ * For normal outputs (Resource, ResourceMap, BinaryPartitioned, JsonPartitioned),
345
+ * only the output name should be provided.
346
+ * For Xsv outputs, both output name and column name should be provided.
347
+ *
348
+ * @param name - Name of the output
349
+ * @param column - Optional. For Xsv outputs, name of the column
350
+ * @returns data resource
351
+ */
352
+ outputData: func(name, ...column) {
353
+ output := outputsMap[name]
354
+ if is_undefined(output) {
355
+ ll.panic("unknown output: " + name)
356
+ }
357
+
358
+ if output.type == "Xsv" {
359
+ if len(column) != 1 {
360
+ ll.panic("column name must be provided for Xsv output")
361
+ }
362
+ return renderResult.output(name + "/" + column[0])
363
+ }
364
+
365
+ if len(column) > 0 {
366
+ ll.panic("column name should not be provided for non-Xsv output")
367
+ }
368
+ return renderResult.output(name)
369
+ },
370
+
371
+ /**
372
+ * Returns both data resource and spec for the given output name and optional column name
373
+ *
374
+ * For normal outputs (Resource, ResourceMap, BinaryPartitioned, JsonPartitioned),
375
+ * only the output name should be provided.
376
+ * For Xsv outputs, both output name and column name should be provided.
377
+ *
378
+ * @param name - Name of the output
379
+ * @param column - Optional. For Xsv outputs, name of the column
380
+ * @returns {data: Resource, spec: PColumnSpec} Object containing both data resource and spec
381
+ */
382
+ output: func(name, ...column) {
383
+ output := outputsMap[name]
384
+ if is_undefined(output) {
385
+ ll.panic("unknown output: " + name)
386
+ }
387
+
388
+ if output.type == "Xsv" {
389
+ if len(column) != 1 {
390
+ ll.panic("column name must be provided for Xsv output")
391
+ }
392
+ columnName := column[0]
393
+ spec := outputSpecs[name][columnName]
394
+ if is_undefined(spec) {
395
+ ll.panic("unknown column: " + columnName)
396
+ }
397
+ return {
398
+ data: renderResult.output(name + "/" + columnName),
399
+ spec: spec
400
+ }
401
+ }
402
+
403
+ if len(column) > 0 {
404
+ ll.panic("column name should not be provided for non-Xsv output")
405
+ }
406
+ return {
407
+ data: renderResult.output(name),
408
+ spec: outputSpecs[name]
409
+ }
410
+ },
411
+
412
+ /**
413
+ * Returns a PFrame containing all columns from an Xsv output
414
+ *
415
+ * @param name - Name of the Xsv output
416
+ * @returns PFrame containing all columns from the output
417
+ */
418
+ xsvOutputFrame: func(name) {
419
+ output := outputsMap[name]
420
+ if is_undefined(output) {
421
+ ll.panic("unknown output: " + name)
422
+ }
423
+
424
+ if output.type != "Xsv" {
425
+ ll.panic("outputFrame can only be used with Xsv outputs")
426
+ }
427
+
428
+ builder := builder.pFrameBuilder()
429
+ for columnName, spec in outputSpecs[name] {
430
+ builder.add(columnName, spec, renderResult.output(name + "/" + columnName))
431
+ }
432
+ return builder.build()
433
+ },
35
434
 
36
- add: func(column, spec, data) {
37
- if (!is_undefined(spec)) {
38
- r.createInputField(column + ".spec").setRefOrJson(spec)
435
+ /**
436
+ * Returns a PFrame containing all results, flattening Xsv outputs using the provided separator
437
+ *
438
+ * @param ...xsvNameSeparator - Optional separator for joining Xsv output and column names, defaults to "."
439
+ * @returns PFrame containing all results
440
+ */
441
+ allOutputsFrame: func(...xsvNameSeparator) {
442
+ sep := "."
443
+ if len(xsvNameSeparator) > 0 {
444
+ sep = xsvNameSeparator[0]
39
445
  }
40
- if (!is_undefined(data)) {
41
- r.createInputField(column + ".data").setRefOrJson(data)
446
+
447
+ builder := builder.pFrameBuilder()
448
+
449
+ for name, output in outputsMap {
450
+ if output.type == "Xsv" {
451
+ // For Xsv outputs, add each column with name prefixed
452
+ for columnName, spec in outputSpecs[name] {
453
+ builder.add(
454
+ name + sep + columnName,
455
+ spec,
456
+ renderResult.output(name + "/" + columnName)
457
+ )
458
+ }
459
+ } else {
460
+ // For non-Xsv outputs, add directly
461
+ builder.add(
462
+ name,
463
+ outputSpecs[name],
464
+ renderResult.output(name)
465
+ )
466
+ }
42
467
  }
43
- return self
468
+
469
+ return builder.build()
44
470
  },
45
471
 
46
- build: func() {
47
- return r.lockAndBuild()
472
+ /**
473
+ * Returns sorted list of all available output names
474
+ *
475
+ * @returns Array of output names
476
+ */
477
+ listOutputs: func() {
478
+ outputs := []
479
+ for name, _ in outputsMap {
480
+ outputs = append(outputs, name)
481
+ }
482
+ return slices.quickSort(outputs)
483
+ },
484
+
485
+ /**
486
+ * Checks if given output is of Xsv type
487
+ *
488
+ * @param name - Output name to check
489
+ * @returns true if output is Xsv, false otherwise
490
+ */
491
+ isXsvOutput: func(name) {
492
+ output := outputsMap[name]
493
+ if is_undefined(output) {
494
+ ll.panic("unknown output: " + name)
495
+ }
496
+ return output.type == "Xsv"
497
+ },
498
+
499
+ /**
500
+ * Returns sorted list of column names for given Xsv output
501
+ *
502
+ * @param name - Xsv output name
503
+ * @returns Array of column names
504
+ */
505
+ listXsvColumns: func(name) {
506
+ output := outputsMap[name]
507
+ if is_undefined(output) {
508
+ ll.panic("unknown output: " + name)
509
+ }
510
+ if output.type != "Xsv" {
511
+ ll.panic("output is not Xsv: " + name)
512
+ }
513
+
514
+ columns := []
515
+ for columnName, _ in outputSpecs[name] {
516
+ columns = append(columns, columnName)
517
+ }
518
+ return slices.quickSort(columns)
48
519
  }
49
- }))
50
- return self
51
- }
520
+ })
52
521
 
53
- /**
54
- * Creates a builder for a resource map p-column's data.
55
- */
56
- pColumnResourceMapDataBuilder := func(keyLength) {
57
- ll.panic("pframes.pColumnResourceMapDataBuilder is not supported any more; use pcolumn.resourceMapBuilder")
522
+ return self
58
523
  }
59
524
 
60
525
  export ll.toStrict({
526
+ processColumn: processColumn,
61
527
  exportFrame: exportFrame,
62
- pFrameBuilder: pFrameBuilder,
528
+ pFrameBuilder: builder.pFrameBuilder,
63
529
  exportColumnData: exportColumnData,
64
530
  pColumnResourceMapDataBuilder: pColumnResourceMapDataBuilder
65
531
  })
@@ -7,62 +7,68 @@ assets := import("@platforma-sdk/workflow-tengo:assets")
7
7
  smart := import("@platforma-sdk/workflow-tengo:smart")
8
8
  render := import("@platforma-sdk/workflow-tengo:render")
9
9
 
10
- llAggTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:pframes.aggregate")
10
+ llProcessTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:pframes.process-pcolumn-data")
11
11
 
12
12
  /**
13
- * Instantiate an aggregation pipeline for the given inputs, and returns rendered ephemeral template.
14
- * Use r.output("output_name") method to get corresponding result, where r is an object returned by
13
+ * Instantiates an aggregation pipeline for the given inputs and returns rendered ephemeral template.
14
+ * Use result.output("outputName") to get corresponding output, where result is the object returned by
15
15
  * this method.
16
16
  *
17
17
  * Contract for the body template:
18
- *
19
18
  * Inputs:
20
- * "__key__" - will be set to the json representation of the group key (i.e. keys in all
21
- * positions not specified in the indices parameter)
22
- * "__value__" - "PColumnData/ResourceMap" resource with all the records in the current group
23
- * ...extra - all values or references passed to optional last parameter of this method
24
- * will be forwarded to the template as is
25
- *
26
- * Outputs:
27
- * array of output names to collect from aggregation operations is specified as a parameter
28
- * of this operation
29
- *
30
- * @param data data resource (currently only PColumnData/ResourceMap is supported)
31
- * @param indices indices to aggregate over (these indices will be stripped from the input map,
32
- * meaning of this parameter is the opposite of GROUP BY from SQL)
33
- * @param bodyTpl reference to the template implementing particular aggregation (see contract above)
34
- * @param outputs array of output settings to to collect from rendered templates
35
- *
36
- * Common fields for all subtypes:
37
- * {
38
- * name: string - name of the output
39
- * path?: string[] - [optional] overrides the path inside the body template
40
- * result, first name is interpreted as output name. If
41
- * not specified path equals to `[name]`.
42
- * }
19
+ * "__key__": string - JSON representation of the group key (i.e. keys in all positions not
20
+ * specified in the indices parameter). Only provided if passKey is true.
21
+ * "__value__": Resource - PColumnData/ResourceMap resource containing all records in current group
22
+ * ...extra - Additional values/references passed via the extra parameter are forwarded
23
+ * to the template unchanged
43
24
  *
44
- * Variants:
45
- * | { type: "Resource" } // - assume normal singular resource is produced by
46
- * this output
47
- * | {
48
- * type: "ResourceMap",
49
- * keyLength: number // - expected key length of result from each operation
50
- * } // - assume each record is ResourceMap
51
- * | {
52
- * type: "BinaryPartitioned" | "JsonPartitioned",
53
- * partitionKeyLength: number // - expected partition length of result from
54
- * each operation
55
- * } // - assume each record is a partitionded Json or Binary PColumn data itself
25
+ * @param data: Resource - Data resource (currently only PColumnData/ResourceMap supported)
26
+ * @param aggregationIndices: number[]|undefined
27
+ * - Indices to aggregate over. These indices will be stripped from
28
+ * the input map (opposite of SQL GROUP BY). If undefined, works as
29
+ * "map" and passes each input value as raw resource.
30
+ * @param bodyTpl: Template - Template implementing the aggregation logic
31
+ * @param outputs: OutputSpec[] - Array of output specifications to collect from rendered templates
32
+ * Common fields for all output types:
33
+ * {
34
+ * name: string - Name of the output
35
+ * path?: string[] - Optional. Overrides path inside body template
36
+ * result. First element is output name. Defaults
37
+ * to [name] if not specified.
38
+ * }
39
+ * Output type variants:
40
+ * | { type: "Resource" } - Normal singular resource
41
+ * | { - ResourceMap output
42
+ * type: "ResourceMap",
43
+ * keyLength: number - Expected key length per operation
44
+ * }
45
+ * | { - Partitioned data output
46
+ * type: "BinaryPartitioned"|"JsonPartitioned",
47
+ * partitionKeyLength: number - Expected partition length per op
48
+ * }
49
+ * | { - CSV/TSV text file output
50
+ * type: "Xsv",
51
+ * xsvType: "tsv"|"csv",
52
+ * flattenWithDelimiter?: string,
53
+ * - If present, flattens output by adding columns
54
+ * one by one using the specified delimiter:
55
+ * "{outputName}{flattenWithDelimiter}{columnId}"
56
+ * settings: object - Xsv conversion specification
57
+ * }
58
+ * @param isEph: boolean - If true, body template will be rendered as ephemeral
59
+ * @param passKey: boolean - If true, body template will receive __key__ input
60
+ * @param extra: object - Optional map of parameters to pass to the template
56
61
  *
57
- * @param isEph if true, body template will be rendered as ephemeral
58
- * @param extra optional map of parameters to pass to the template
62
+ * @returns RenderResult - Rendered template result with output() method for accessing outputs
63
+ * @panics - If extra parameter count is invalid
59
64
  */
60
- aggregate := func(data, indices, bodyTpl, outputs, isEph, ...extra) {
65
+ aggregateV2 := func(data, aggregationIndices, bodyTpl, outputs, isEph, passKey, ...extra) {
61
66
  renderInputs := {
62
67
  params: smart.createJsonResource({
63
- indices: indices,
68
+ aggregationIndices: aggregationIndices,
64
69
  eph: isEph,
65
- outputs: outputs
70
+ outputs: outputs,
71
+ passKey: passKey
66
72
  }),
67
73
  body: bodyTpl,
68
74
  data: data
@@ -76,10 +82,18 @@ aggregate := func(data, indices, bodyTpl, outputs, isEph, ...extra) {
76
82
  ll.panic("Wrong number of parameters for aggregate method.")
77
83
  }
78
84
 
79
- renderResult := render.createEphemeral(llAggTpl, renderInputs)
80
- return renderResult;
85
+ renderResult := render.createEphemeral(llProcessTpl, renderInputs)
86
+ return renderResult
87
+ }
88
+
89
+ /*
90
+ * Read docs for aggregateV2
91
+ */
92
+ aggregate := func(data, aggregationIndices, bodyTpl, outputs, isEph, ...extra) {
93
+ return aggregateV2(data, aggregationIndices, bodyTpl, outputs, isEph, true, extra...)
81
94
  }
82
95
 
83
96
  export ll.toStrict({
84
- aggregate: aggregate
97
+ aggregate: aggregate,
98
+ aggregateV2: aggregateV2
85
99
  })
@@ -68,6 +68,133 @@ A_LABEL := "pl7.app/label"
68
68
  */
69
69
  A_TRACE := "pl7.app/trace"
70
70
 
71
+ //
72
+ // Format methods
73
+ //
74
+
75
+ normalizeColumnSpec := func(spec) {
76
+ if is_undefined(spec.axesSpec) {
77
+ spec.axesSpec = []
78
+ }
79
+ validation.assertType(spec, P_COLUMN_SPEC_SCHEMA)
80
+ return spec
81
+ }
82
+
83
+ //
84
+ // Matchers
85
+ //
86
+
87
+ _P_MATCHER_SCHEMA := {
88
+ `__options__,closed`: ``,
89
+ `type,?`: `string,regex=Int|Long|Float|Double|String`,
90
+ `name`: `string`,
91
+ `domain,?`: { any: `string` },
92
+ `optional,?`: `boolean`
93
+ }
94
+
95
+ _normalizeMatcher := func(matcher) {
96
+ if is_string(matcher) {
97
+ return { name: matcher }
98
+ } else {
99
+ validation.assertType(matcher, _P_MATCHER_SCHEMA)
100
+ return matcher
101
+ }
102
+ }
103
+
104
+ _checkAxisMatch := func(axisSpec, matcher) {
105
+ if axisSpec.name != matcher.name {
106
+ return false
107
+ }
108
+ if !is_undefined(matcher.type) && axisSpec.type != matcher.type {
109
+ return false
110
+ }
111
+ if !is_undefined(matcher.domain) {
112
+ for domain, domainValue in matcher.domain {
113
+ if is_undefined(axisSpec.domain) || axisSpec.domain[domain] != domainValue {
114
+ return false
115
+ }
116
+ }
117
+ }
118
+ return true
119
+ }
120
+
121
+ /**
122
+ * For a given array of axes specs returns the index of matching axis:
123
+ * - if multiple matches are found - panics
124
+ * - if axis not found and matcher is not optional - panics
125
+ */
126
+ matchAxis := func(axesSpec, matcher) {
127
+ validation.assertType(axesSpec, [ P_AXIS_SPEC_SCHEMA ])
128
+ matcher = _normalizeMatcher(matcher)
129
+ found := -1
130
+ for idx, axisSpec in axesSpec {
131
+ if !_checkAxisMatch(axisSpec, matcher) {
132
+ continue
133
+ }
134
+ if found != -1 {
135
+ ll.panic("multiple matches found for %v in %v", matcher, axesSpec)
136
+ }
137
+ found = idx
138
+ }
139
+ if found == -1 && !matcher.optional {
140
+ ll.panic("match not found for %v in %v", matcher, axesSpec)
141
+ }
142
+ return found
143
+ }
144
+
145
+ /**
146
+ * Matches axes specs against matchers and returns array of matched indices.
147
+ *
148
+ * Example matchers:
149
+ * - "pl7.app/sequencing/readIndex"
150
+ * - { name: "pl7.app/sequencing/readIndex" }
151
+ * - { type: "String", name: "pl7.app/sequencing/lane" }
152
+ * - { type: "String", name: "pl7.app/sequencing/lane", optional: true }
153
+ * - { type: "String", name: "pl7.app/rna-seq/geneId", domain: { "pl7.app/species": "homo-sapiens" } }
154
+ *
155
+ * @param axesSpec: AxisSpec[] Array of axis specifications to match against
156
+ * @param matchers: (string|AxisMatcher)[] Array of matchers, each can be either:
157
+ * - string: Simple name matcher e.g. "pl7.app/sequencing/readIndex"
158
+ * - AxisMatcher: Object with matching criteria:
159
+ * - name: string - Axis name to match
160
+ * - type?: string - Optional axis type to match
161
+ * - domain?: Record<string,string> - Optional domain criteria
162
+ * - optional?: boolean - If true, won't panic when no match found
163
+ * @param opts?: object Optional parameters:
164
+ * - keepUnmatched?: boolean - Whether to include unmatched indices (-1) in result
165
+ * @returns number[] Array of matched indices in same order as matchers. Contains -1 for
166
+ * unmatched optional matchers if keepUnmatched is true.
167
+ * @panics
168
+ * - If multiple axes match a single matcher
169
+ * - If no match found for non-optional matcher
170
+ * - If multiple matchers match the same axis
171
+ */
172
+ matchAxes := func(axesSpec, matchers, ...opts) {
173
+ keepUnmatched := false
174
+ if len(opts) == 1 {
175
+ keepUnmatched = opts[0].keepUnmatched
176
+ } else if len(opts) != 0 {
177
+ ll.panic("wrong number of parameters for matchAxes")
178
+ }
179
+
180
+ result := []
181
+ resultSet := {}
182
+ for matcher in matchers {
183
+ idx := matchAxis(axesSpec, matcher)
184
+ if idx != -1 {
185
+ idxS := string(idx)
186
+ if resultSet[idxS] {
187
+ ll.panic("duplicate match detecterd for %v in %v (idx = %v)", matchers, axesSpec, idx)
188
+ }
189
+ resultSet[idxS] = true
190
+ }
191
+ if idx != -1 || keepUnmatched {
192
+ result = append(result, idx)
193
+ }
194
+ }
195
+ return result
196
+ }
197
+
71
198
  //
72
199
  // Utility methods
73
200
  //
@@ -123,6 +250,9 @@ makeTrace := func(input, ...steps) {
123
250
  }
124
251
 
125
252
  export ll.toStrict({
253
+ P_AXIS_SPEC_SCHEMA: P_AXIS_SPEC_SCHEMA,
254
+ P_COLUMN_SPEC_SCHEMA: P_COLUMN_SPEC_SCHEMA,
255
+
126
256
  KIND_P_COLUMN: KIND_P_COLUMN,
127
257
 
128
258
  N_LABEL: N_LABEL,
@@ -132,5 +262,10 @@ export ll.toStrict({
132
262
  A_LABEL: A_LABEL,
133
263
  A_TRACE: A_TRACE,
134
264
 
265
+ normalizeColumnSpec: normalizeColumnSpec,
266
+
267
+ matchAxis: matchAxis,
268
+ matchAxes: matchAxes,
269
+
135
270
  makeTrace: makeTrace
136
271
  })
@@ -1,6 +1,7 @@
1
1
  ll := import("@platforma-sdk/workflow-tengo:ll")
2
2
  validation := import("@platforma-sdk/workflow-tengo:validation")
3
3
  objects := import("@platforma-sdk/workflow-tengo:objects")
4
+ slices := import("@platforma-sdk/workflow-tengo:slices")
4
5
 
5
6
  _PRE_PROCESS_STEP_SCHEMA := [`or`,
6
7
  {
@@ -317,11 +318,46 @@ decomposePfconvImportCfg := func(cfg, ...ops) {
317
318
  }
318
319
  }
319
320
 
321
+ EMPTY_JSON_KEY := "[]" // string(json.encode([]))
322
+
323
+ /**
324
+ * Calculates group indices from aggregation indices by removing aggregation indices from the full set of indices.
325
+ * If no aggregation indices are provided, returns all indices.
326
+ *
327
+ * @param numberOfAxes: number - Total number of axes in the input
328
+ * @param aggregationIndices: number[]|undefined - Indices to aggregate over. If undefined, returns all indices.
329
+ *
330
+ * @returns number[] - Array of remaining indices after removing aggregation indices, in ascending order.
331
+ * If aggregationIndices is undefined, returns array [0..numberOfAxes-1]
332
+ */
333
+ calculateGroupAxesIndices := func(numberOfAxes, aggregationIndices) {
334
+ groupIndices := []
335
+ for i:=0; i<numberOfAxes; i++ {
336
+ groupIndices = append(groupIndices, i)
337
+ }
338
+
339
+ // groupIndices at this point is [0, 1, 2, ... numberOfAxes-1]
340
+
341
+ if is_undefined(aggregationIndices) {
342
+ return groupIndices
343
+ }
344
+
345
+ // copy and sort to prevent mutation of original array
346
+ aggregationIndices = slices.quickSort(aggregationIndices)
347
+
348
+ for j:=len(aggregationIndices)-1; j>=0; j-- {
349
+ splice(groupIndices, aggregationIndices[j], 1)
350
+ }
351
+ return groupIndices
352
+ }
353
+
320
354
  export ll.toStrict({
321
355
  PFCONV_IMPORT_CFG_SCHEMA: PFCONV_IMPORT_CFG_SCHEMA,
322
356
  PFCONV_EXPORT_CFG_SCHEMA: PFCONV_EXPORT_CFG_SCHEMA,
357
+ EMPTY_JSON_KEY: EMPTY_JSON_KEY,
323
358
 
324
359
  xsvColumnId: xsvColumnId,
325
360
  purifySpec: purifySpec,
326
- decomposePfconvImportCfg: decomposePfconvImportCfg
361
+ decomposePfconvImportCfg: decomposePfconvImportCfg,
362
+ calculateGroupAxesIndices: calculateGroupAxesIndices
327
363
  })
@@ -83,7 +83,7 @@ getColumnSpec := func(axesSpec, col) {
83
83
  * @return map: reference - a reference to a map resource storing imported data.
84
84
  */
85
85
  importFile := func(xsvFile, xsvType, spec, ...ops) {
86
- ll.assert(xsvType == "csv" || xsvType == "tsv", "expected one of [tsv, csv] types")
86
+ ll.assert(xsvType == "csv" || xsvType == "tsv", "expected one of [tsv, csv] types, found: " + xsvType)
87
87
 
88
88
  validation.assertType(spec, util.PFCONV_IMPORT_CFG_SCHEMA)
89
89
 
@@ -294,7 +294,7 @@ exportFrame := func(pf, xsvType, params, ...ops) {
294
294
  }
295
295
  pf = pfm
296
296
  }
297
-
297
+
298
298
  r := render.create(exportPfTpl, {
299
299
  pf: pf,
300
300
  xsvType: xsvType,
@@ -286,6 +286,20 @@ fromSet := func(set) {
286
286
  return sortByFn(slice, func(elem) { return elem })
287
287
  }
288
288
 
289
+ /**
290
+ * Normalizes a slice by ensuring it is never undefined.
291
+ * Returns an empty slice if input is undefined, otherwise returns the input slice unchanged.
292
+ *
293
+ * @param s: any[]|undefined - Input slice that may be undefined
294
+ * @returns any[] - Normalized slice, never undefined
295
+ */
296
+ normalize := func(s) {
297
+ if is_undefined(s) {
298
+ return []
299
+ }
300
+ return s
301
+ }
302
+
289
303
  export ll.toStrict({
290
304
  length: length,
291
305
  safeAppend: safeAppend,
@@ -301,5 +315,6 @@ export ll.toStrict({
301
315
  quickSortInPlaceFn: quickSortInPlaceFn,
302
316
  sortByFn: sortByFn,
303
317
  fromSet: fromSet,
304
- slice: slice
318
+ slice: slice,
319
+ normalize: normalize
305
320
  })
@@ -118,7 +118,7 @@ _validateTypes := func(js, tags, keyPath, ...stack) {
118
118
  return success
119
119
  }
120
120
 
121
- if k == "bool" && is_bool(js) {
121
+ if (k == "bool" || k == "boolean") && is_bool(js) {
122
122
  return success
123
123
  }
124
124
 
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/workflow-tengo",
3
- "version": "2.12.1",
3
+ "version": "2.13.0",
4
4
  "description": "Tengo SDK library",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./dist/index.js",