Skip to content

Commit 3120e8a

Browse files
committed
fix: allow all inputs in scripts
1 parent af5e89b commit 3120e8a

File tree

3 files changed

+143
-43
lines changed

3 files changed

+143
-43
lines changed

lib/zeebe/util/feelUtility.js

Lines changed: 100 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,21 @@ export function parseVariables(variables) {
3535
return;
3636
}
3737

38-
const { expression, unresolved, analysis, isInputExpression } = expressionDetails;
38+
const {
39+
expression,
40+
unresolved,
41+
requirementAnalyses
42+
} = expressionDetails;
3943

4044
variablesToResolve.push({ variable, expression, unresolved });
4145

42-
// Collect analysis for input requirement extraction
43-
if (isInputExpression && analysis && analysis.inputs.length > 0) {
46+
// Collect analyses for input requirement extraction
47+
for (const { expressionType, inputs } of requirementAnalyses) {
4448
analysisResults.push({
4549
origin,
4650
targetName: variable.name,
47-
inputs: analysis.inputs,
48-
isLocalMapping: variable.scope === origin
51+
inputs,
52+
expressionType
4953
});
5054
}
5155
});
@@ -184,73 +188,125 @@ export function getResultContext(expression, variables = {}) {
184188
return latestVariables;
185189
}
186190

191+
/**
192+
* Find all matching expression sources for a variable on a given origin element.
193+
*
194+
* Returns candidates ordered by resolution priority (first = highest).
195+
* Local variables: script > input-mapping (script result overwrites at runtime).
196+
* Global variables: output-mapping > script.
197+
*
198+
* @param {ProcessVariable} variable
199+
* @param {djs.model.Base} origin
200+
* @returns {Array<{ type: string, value: string }>}
201+
*/
202+
function findExpressions(variable, origin) {
203+
const isLocal = variable.scope === origin;
204+
205+
const candidates = isLocal
206+
? [
207+
{ type: 'script', value: getScriptExpression(variable, origin) },
208+
{ type: 'input-mapping', value: getIoInputExpression(variable, origin) },
209+
]
210+
: [
211+
{ type: 'output-mapping', value: getIoOutputExpression(variable, origin) },
212+
{ type: 'script', value: getScriptExpression(variable, origin) },
213+
];
214+
215+
return candidates.filter(c => c.value);
216+
}
217+
187218
/**
188219
* Given a Variable and a specific origin, return the mapping expression and all
189220
* unresolved variables used in that expression. Returns undefined if no mapping
190221
* exists for the given origin.
191222
*
223+
* The primary (highest-priority) expression is used for variable resolution.
224+
* All non-output-mapping expressions are analyzed for input requirements,
225+
* since a variable may be produced by both a script and an input mapping.
226+
*
192227
* @param {ProcessVariable} variable
193228
* @param {djs.model.Base} origin
194-
* @returns {{ expression: String, unresolved: Array<String>, analysis: Object }}}
229+
* @returns {{ expression: String, unresolved: Array<String>, requirementAnalyses: Array, expressionType: String }}
195230
*/
196231
function getExpressionDetails(variable, origin) {
197232

198-
// if variable scope is !== origin (global), prioritize IoExpression over ScriptExpression
199-
// if variable scope is === origin (local), prioritize ScriptExpression over IoExpression
200-
const expression = variable.scope !== origin
201-
? getIoExpression(variable, origin) || getScriptExpression(variable, origin)
202-
: getScriptExpression(variable, origin) || getIoExpression(variable, origin);
203-
204-
// Output mappings don't produce input requirements
205-
const isInputExpression = variable.scope !== origin
206-
? !getIoExpression(variable, origin)
207-
: true;
233+
const matches = findExpressions(variable, origin);
208234

209-
if (!expression) {
235+
if (matches.length === 0) {
210236
return;
211237
}
212238

213-
const result = getResultContext(expression);
239+
// Primary expression for variable resolution (highest priority)
240+
const { value: expression, type: expressionType } = matches[0];
214241

242+
const result = getResultContext(expression);
215243
const unresolved = findUnresolvedVariables(result);
216244

217-
// Analyze the expression to extract input requirements
218-
let analysis;
219-
try {
220-
const analysisResult = feelAnalyzer.analyzeExpression(`=${expression}`);
221-
if (analysisResult.valid !== false) {
222-
analysis = {
223-
inputs: analysisResult.inputs || []
224-
};
245+
// Analyze each non-output-mapping expression for input requirements
246+
const requirementAnalyses = [];
247+
248+
for (const { type, value } of matches) {
249+
if (type === 'output-mapping') {
250+
continue;
251+
}
252+
253+
try {
254+
const analysisResult = feelAnalyzer.analyzeExpression(`=${value}`);
255+
256+
if (analysisResult.valid !== false && analysisResult.inputs && analysisResult.inputs.length > 0) {
257+
requirementAnalyses.push({
258+
expressionType: type,
259+
inputs: analysisResult.inputs
260+
});
261+
}
262+
} catch (error) {
263+
console.warn(`Failed to analyze expression for variable ${variable.name}:`, error);
225264
}
226-
} catch (error) {
227-
console.warn(`Failed to analyze expression for variable ${variable.name}:`, error);
228265
}
229266

230-
return { expression, unresolved, analysis, isInputExpression };
267+
return { expression, unresolved, requirementAnalyses, expressionType };
268+
}
269+
270+
/**
271+
* Given a variable and origin, return input mapping expression targeting the variable.
272+
*
273+
* @param {ProcessVariable} variable
274+
* @param {djs.model.Base} origin
275+
* @returns {string|undefined}
276+
*/
277+
function getIoInputExpression(variable, origin) {
278+
return getIoExpressionByType(variable, origin, 'input');
231279
}
232280

233281
/**
234-
* Given a Variable and a specific origin, return the mapping expression for all
235-
* input outputs mapping. Returns undefined if no mapping exists for the given origin.
282+
* Given a variable and origin, return output mapping expression targeting the variable.
236283
*
237284
* @param {ProcessVariable} variable
238285
* @param {djs.model.Base} origin
239-
* @returns { expression: String}
286+
* @returns {string|undefined}
240287
*/
241-
function getIoExpression(variable, origin) {
288+
function getIoOutputExpression(variable, origin) {
289+
return getIoExpressionByType(variable, origin, 'output');
290+
}
291+
292+
/**
293+
* Given a variable and origin, return mapping expression by mapping type.
294+
*
295+
* @param {ProcessVariable} variable
296+
* @param {djs.model.Base} origin
297+
* @param {'input'|'output'} mappingType
298+
* @returns {string|undefined}
299+
*/
300+
function getIoExpressionByType(variable, origin, mappingType) {
242301
const ioMapping = getExtensionElementsList(origin, 'zeebe:IoMapping')[0];
243302

244303
if (!ioMapping) {
245304
return;
246305
}
247306

248-
let mappings;
249-
if (origin === variable.scope) {
250-
mappings = ioMapping.inputParameters;
251-
} else {
252-
mappings = ioMapping.outputParameters;
253-
}
307+
const mappings = mappingType === 'input'
308+
? ioMapping.inputParameters
309+
: ioMapping.outputParameters;
254310

255311
if (!mappings) {
256312
return;
@@ -263,7 +319,6 @@ function getIoExpression(variable, origin) {
263319
}
264320

265321
return mapping.source.substring(1);
266-
267322
}
268323

269324
/**
@@ -482,22 +537,24 @@ export function getElementNamesToRemove(moddleElement, inputOutput) {
482537
/**
483538
* Build input requirement variables from pre-collected analysis results.
484539
*
485-
* @param {Array<{ origin: Object, targetName: String, inputs: Array, isLocalMapping: boolean }>} analysisResults
540+
* @param {Array<{ origin: Object, targetName: String, inputs: Array, expressionType: String }>} analysisResults
486541
* @returns {Array<ProcessVariable>} input requirements
487542
*/
488543
function buildInputRequirements(analysisResults) {
489544
const inputRequirements = {};
490545
const inputMappingTargetsCache = {};
491546

492-
for (const { origin, targetName, inputs, isLocalMapping } of analysisResults) {
547+
for (const { origin, targetName, inputs, expressionType } of analysisResults) {
493548

494549
if (!inputMappingTargetsCache[origin.id]) {
495550
inputMappingTargetsCache[origin.id] = getInputMappingTargetNames(origin);
496551
}
497552
const orderedTargets = inputMappingTargetsCache[origin.id];
498553

554+
// Input mappings are order-sensitive: only earlier targets are available.
555+
// Scripts can reference all input mapping targets.
499556
let availableLocalTargets;
500-
if (isLocalMapping) {
557+
if (expressionType === 'input-mapping') {
501558
const targetIndex = orderedTargets.indexOf(targetName);
502559
availableLocalTargets = new Set(orderedTargets.slice(0, targetIndex));
503560
} else {

test/fixtures/zeebe/mappings/script-task-with-input-mappings.bpmn

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@
4141
<zeebe:script expression="=a + b" resultVariable="shadowChainResult" />
4242
</bpmn:extensionElements>
4343
</bpmn:scriptTask>
44+
<bpmn:scriptTask id="scriptUsesSecondInput" name="Script uses second input">
45+
<bpmn:extensionElements>
46+
<zeebe:ioMapping>
47+
<zeebe:input source="=def" target="abc" />
48+
<zeebe:input source="=1" target="test" />
49+
</zeebe:ioMapping>
50+
<zeebe:script expression="=test" resultVariable="abc" />
51+
</bpmn:extensionElements>
52+
</bpmn:scriptTask>
4453
</bpmn:process>
4554
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
4655
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
@@ -59,6 +68,9 @@
5968
<bpmndi:BPMNShape id="Shape_5" bpmnElement="shadowingChainedTask">
6069
<dc:Bounds x="750" y="80" width="100" height="80" />
6170
</bpmndi:BPMNShape>
71+
<bpmndi:BPMNShape id="Shape_6" bpmnElement="scriptUsesSecondInput">
72+
<dc:Bounds x="900" y="80" width="100" height="80" />
73+
</bpmndi:BPMNShape>
6274
</bpmndi:BPMNPlane>
6375
</bpmndi:BPMNDiagram>
6476
</bpmn:definitions>

test/spec/zeebe/Mappings.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,22 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
855855
expect(names).to.not.include('b');
856856
}));
857857

858+
859+
it('should not require second input mapping variable used in script', inject(async function(variableResolver) {
860+
861+
// when
862+
const variables = (await variableResolver.getVariables())['Process_1'];
863+
const reqs = variables.filter(v =>
864+
v.usedBy && v.usedBy.length > 0 && v.origin[0].id === 'scriptUsesSecondInput'
865+
);
866+
const names = reqs.map(v => v.name);
867+
868+
// then
869+
expect(names).to.include('def');
870+
expect(names).to.not.include('test');
871+
expect(reqs).to.have.length(1);
872+
}));
873+
858874
});
859875

860876

@@ -1095,6 +1111,21 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
10951111
expect(requirements).to.have.length(1);
10961112
}));
10971113

1114+
1115+
it('should allow script to use all input targets regardless of order', inject(async function(variableResolver, elementRegistry) {
1116+
1117+
// when
1118+
const requirements = await variableResolver.getInputRequirementsForElement(
1119+
elementRegistry.get('scriptUsesSecondInput')
1120+
);
1121+
1122+
// then
1123+
const names = requirements.map(v => v.name);
1124+
expect(names).to.include('def');
1125+
expect(names).to.not.include('test');
1126+
expect(requirements).to.have.length(1);
1127+
}));
1128+
10981129
});
10991130

11001131

0 commit comments

Comments
 (0)