@platforma-sdk/workflow-tengo 2.12.0 → 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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/index.mjs +3 -3
- package/dist/tengo/lib/pframes.builder.lib.tengo +35 -0
- package/dist/tengo/lib/pframes.constants.lib.tengo +14 -0
- package/dist/tengo/lib/pframes.lib.tengo +488 -22
- package/dist/tengo/lib/pframes.ll.lib.tengo +61 -47
- package/dist/tengo/lib/pframes.spec.lib.tengo +135 -0
- package/dist/tengo/lib/pframes.util.lib.tengo +37 -1
- package/dist/tengo/lib/pframes.xsv.lib.tengo +2 -2
- package/dist/tengo/lib/slices.lib.tengo +16 -1
- package/dist/tengo/lib/validation.lib.tengo +1 -1
- package/dist/tengo/tpl/exec.R.create-renv.plj.gz +0 -0
- package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
- package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
- package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
- package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.map-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.process-pcolumn-data.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
- package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
- package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
- package/package.json +2 -2
- 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.
|
|
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
|
-
|
|
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-
|
|
35
|
+
* Creates a builder for a resource map p-column's data.
|
|
29
36
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
468
|
+
|
|
469
|
+
return builder.build()
|
|
44
470
|
},
|
|
45
471
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
10
|
+
llProcessTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:pframes.process-pcolumn-data")
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* Use
|
|
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__"
|
|
21
|
-
*
|
|
22
|
-
* "__value__" -
|
|
23
|
-
* ...extra
|
|
24
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
* @
|
|
58
|
-
* @
|
|
62
|
+
* @returns RenderResult - Rendered template result with output() method for accessing outputs
|
|
63
|
+
* @panics - If extra parameter count is invalid
|
|
59
64
|
*/
|
|
60
|
-
|
|
65
|
+
aggregateV2 := func(data, aggregationIndices, bodyTpl, outputs, isEph, passKey, ...extra) {
|
|
61
66
|
renderInputs := {
|
|
62
67
|
params: smart.createJsonResource({
|
|
63
|
-
|
|
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(
|
|
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
|
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/workflow-tengo",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Tengo SDK library",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./dist/**/*"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@milaboratories/software-pframes-conv": "2.1.
|
|
19
|
+
"@milaboratories/software-pframes-conv": "2.1.12",
|
|
20
20
|
"@platforma-open/milaboratories.software-small-binaries": "^1.15.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
Binary file
|