Skip to content

Commit af5e89b

Browse files
committed
fix: follow input mapping chain
1 parent 29a5bdd commit af5e89b

File tree

3 files changed

+167
-31
lines changed

3 files changed

+167
-31
lines changed

lib/zeebe/util/feelUtility.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export function parseVariables(variables) {
2424

2525
const variablesToResolve = [];
2626
const analysisResults = [];
27-
const inputMappingTargets = {};
2827

2928
// Step 1 - Parse all variables and populate all that don't have references
3029
// to other variables
@@ -40,20 +39,13 @@ export function parseVariables(variables) {
4039

4140
variablesToResolve.push({ variable, expression, unresolved });
4241

43-
// Track input mapping targets per origin
44-
if (isInputExpression && variable.scope === origin) {
45-
if (!inputMappingTargets[origin.id]) {
46-
inputMappingTargets[origin.id] = new Set();
47-
}
48-
inputMappingTargets[origin.id].add(variable.name);
49-
}
50-
5142
// Collect analysis for input requirement extraction
5243
if (isInputExpression && analysis && analysis.inputs.length > 0) {
5344
analysisResults.push({
5445
origin,
5546
targetName: variable.name,
56-
inputs: analysis.inputs
47+
inputs: analysis.inputs,
48+
isLocalMapping: variable.scope === origin
5749
});
5850
}
5951
});
@@ -63,7 +55,7 @@ export function parseVariables(variables) {
6355
const resolvedVariables = resolveReferences(variablesToResolve, variables);
6456

6557
// Step 3 - Build input requirements from collected analyses
66-
const inputRequirements = buildInputRequirements(analysisResults, inputMappingTargets);
58+
const inputRequirements = buildInputRequirements(analysisResults);
6759

6860
return { resolvedVariables, inputRequirements };
6961
}
@@ -490,20 +482,32 @@ export function getElementNamesToRemove(moddleElement, inputOutput) {
490482
/**
491483
* Build input requirement variables from pre-collected analysis results.
492484
*
493-
* @param {Array<{ origin: Object, targetName: String, inputs: Array }>} analysisResults
494-
* @param {Object<string, Set<string>>} inputMappingTargets - map of origin id to input mapping target names
485+
* @param {Array<{ origin: Object, targetName: String, inputs: Array, isLocalMapping: boolean }>} analysisResults
495486
* @returns {Array<ProcessVariable>} input requirements
496487
*/
497-
function buildInputRequirements(analysisResults, inputMappingTargets = {}) {
488+
function buildInputRequirements(analysisResults) {
498489
const inputRequirements = {};
490+
const inputMappingTargetsCache = {};
499491

500-
for (const { origin, targetName, inputs } of analysisResults) {
501-
const localTargets = inputMappingTargets[origin.id];
492+
for (const { origin, targetName, inputs, isLocalMapping } of analysisResults) {
493+
494+
if (!inputMappingTargetsCache[origin.id]) {
495+
inputMappingTargetsCache[origin.id] = getInputMappingTargetNames(origin);
496+
}
497+
const orderedTargets = inputMappingTargetsCache[origin.id];
498+
499+
let availableLocalTargets;
500+
if (isLocalMapping) {
501+
const targetIndex = orderedTargets.indexOf(targetName);
502+
availableLocalTargets = new Set(orderedTargets.slice(0, targetIndex));
503+
} else {
504+
availableLocalTargets = new Set(orderedTargets);
505+
}
502506

503507
for (const inputVar of inputs) {
504508

505509
// Skip variables that are provided by input mappings on the same element
506-
if (localTargets && localTargets.has(inputVar.name)) {
510+
if (availableLocalTargets.has(inputVar.name)) {
507511
continue;
508512
}
509513

@@ -534,4 +538,16 @@ function buildInputRequirements(analysisResults, inputMappingTargets = {}) {
534538
return Object.values(inputRequirements);
535539
}
536540

541+
/**
542+
* Get ordered input mapping target names for an element.
543+
*
544+
* @param {djs.model.Base} origin
545+
* @returns {Array<String>}
546+
*/
547+
function getInputMappingTargetNames(origin) {
548+
const ioMapping = getExtensionElementsList(origin, 'zeebe:IoMapping')[0];
549+
if (!ioMapping || !ioMapping.inputParameters) return [];
550+
return ioMapping.inputParameters.map(p => p.target);
551+
}
552+
537553

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,32 @@
1515
<zeebe:script expression="=x + y" resultVariable="scriptResult2" />
1616
</bpmn:extensionElements>
1717
</bpmn:scriptTask>
18+
<bpmn:scriptTask id="chainedInputMappings" name="Chained input mappings">
19+
<bpmn:extensionElements>
20+
<zeebe:ioMapping>
21+
<zeebe:input source="=processVar3" target="localC" />
22+
<zeebe:input source="=localC + 1" target="localD" />
23+
</zeebe:ioMapping>
24+
<zeebe:script expression="=localC + localD" resultVariable="chainedResult" />
25+
</bpmn:extensionElements>
26+
</bpmn:scriptTask>
27+
<bpmn:scriptTask id="shadowingTask" name="Shadowing a to a">
28+
<bpmn:extensionElements>
29+
<zeebe:ioMapping>
30+
<zeebe:input source="=a" target="a" />
31+
</zeebe:ioMapping>
32+
<zeebe:script expression="=a" resultVariable="shadowResult" />
33+
</bpmn:extensionElements>
34+
</bpmn:scriptTask>
35+
<bpmn:scriptTask id="shadowingChainedTask" name="Shadowing then chaining">
36+
<bpmn:extensionElements>
37+
<zeebe:ioMapping>
38+
<zeebe:input source="=a" target="a" />
39+
<zeebe:input source="=a + 1" target="b" />
40+
</zeebe:ioMapping>
41+
<zeebe:script expression="=a + b" resultVariable="shadowChainResult" />
42+
</bpmn:extensionElements>
43+
</bpmn:scriptTask>
1844
</bpmn:process>
1945
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
2046
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
@@ -24,6 +50,15 @@
2450
<bpmndi:BPMNShape id="Shape_2" bpmnElement="scriptWithoutInputs">
2551
<dc:Bounds x="300" y="80" width="100" height="80" />
2652
</bpmndi:BPMNShape>
53+
<bpmndi:BPMNShape id="Shape_3" bpmnElement="chainedInputMappings">
54+
<dc:Bounds x="450" y="80" width="100" height="80" />
55+
</bpmndi:BPMNShape>
56+
<bpmndi:BPMNShape id="Shape_4" bpmnElement="shadowingTask">
57+
<dc:Bounds x="600" y="80" width="100" height="80" />
58+
</bpmndi:BPMNShape>
59+
<bpmndi:BPMNShape id="Shape_5" bpmnElement="shadowingChainedTask">
60+
<dc:Bounds x="750" y="80" width="100" height="80" />
61+
</bpmndi:BPMNShape>
2762
</bpmndi:BPMNPlane>
2863
</bpmndi:BPMNDiagram>
2964
</bpmn:definitions>

test/spec/zeebe/Mappings.spec.js

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -790,9 +790,7 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
790790
const withUsedBy = variables.filter(v => v.usedBy && v.usedBy.length > 0);
791791
const names = withUsedBy.map(v => v.name);
792792

793-
// then - input requirements should come from input mapping expressions only;
794-
// localA and localB are provided by the input mappings, so the script's
795-
// references to them should NOT produce input requirements
793+
// then
796794
expect(names).to.include('processVar1');
797795
expect(names).to.include('processVar2');
798796
expect(names).to.not.include('localA');
@@ -807,11 +805,56 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
807805
const withUsedBy = variables.filter(v => v.usedBy && v.usedBy.length > 0);
808806
const names = withUsedBy.map(v => v.name);
809807

810-
// then - the second script task has no input mappings, so its script variables should be extracted
808+
// then
811809
expect(names).to.include('x');
812810
expect(names).to.include('y');
813811
}));
814812

813+
814+
it('should not require locally provided variable when chained input mapping references earlier target', inject(async function(variableResolver) {
815+
816+
// when
817+
const variables = (await variableResolver.getVariables())['Process_1'];
818+
const chainedReqs = variables.filter(v =>
819+
v.usedBy && v.usedBy.length > 0 && v.origin[0].id === 'chainedInputMappings'
820+
);
821+
const names = chainedReqs.map(v => v.name);
822+
823+
// then
824+
expect(names).to.include('processVar3');
825+
expect(names).to.not.include('localC');
826+
expect(names).to.not.include('localD');
827+
}));
828+
829+
830+
it('should keep shadowed variable as input requirement when mapping a to a', inject(async function(variableResolver) {
831+
832+
// when
833+
const variables = (await variableResolver.getVariables())['Process_1'];
834+
const shadowReqs = variables.filter(v =>
835+
v.usedBy && v.usedBy.length > 0 && v.origin[0].id === 'shadowingTask'
836+
);
837+
const names = shadowReqs.map(v => v.name);
838+
839+
// then
840+
expect(names).to.include('a');
841+
}));
842+
843+
844+
it('should handle shadowing with chaining correctly', inject(async function(variableResolver) {
845+
846+
// when
847+
const variables = (await variableResolver.getVariables())['Process_1'];
848+
const reqs = variables.filter(v =>
849+
v.usedBy && v.usedBy.length > 0 && v.origin[0].id === 'shadowingChainedTask'
850+
);
851+
const names = reqs.map(v => v.name);
852+
853+
// then
854+
expect(names).to.include('a');
855+
expect(names).to.not.include('b');
856+
}));
857+
815858
});
816859

817860

@@ -978,14 +1021,12 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
9781021

9791022
it('should only return process variables as input requirements, not locally mapped ones', inject(async function(variableResolver, elementRegistry) {
9801023

981-
// given
982-
const task = elementRegistry.get('scriptWithInputs');
983-
9841024
// when
985-
const requirements = await variableResolver.getInputRequirementsForElement(task);
1025+
const requirements = await variableResolver.getInputRequirementsForElement(
1026+
elementRegistry.get('scriptWithInputs')
1027+
);
9861028

987-
// then - only processVar1 and processVar2 (from input mapping sources) should
988-
// be requirements, not localA and localB (input mapping targets used in the script)
1029+
// then
9891030
const names = requirements.map(v => v.name);
9901031
expect(names).to.include('processVar1');
9911032
expect(names).to.include('processVar2');
@@ -997,11 +1038,10 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
9971038

9981039
it('should return all script variables for task without input mappings', inject(async function(variableResolver, elementRegistry) {
9991040

1000-
// given
1001-
const task = elementRegistry.get('scriptWithoutInputs');
1002-
10031041
// when
1004-
const requirements = await variableResolver.getInputRequirementsForElement(task);
1042+
const requirements = await variableResolver.getInputRequirementsForElement(
1043+
elementRegistry.get('scriptWithoutInputs')
1044+
);
10051045

10061046
// then
10071047
const names = requirements.map(v => v.name);
@@ -1010,6 +1050,51 @@ describe('ZeebeVariableResolver - Variable Mappings', function() {
10101050
expect(requirements).to.have.length(2);
10111051
}));
10121052

1053+
1054+
it('should handle chained input mappings respecting order', inject(async function(variableResolver, elementRegistry) {
1055+
1056+
// when
1057+
const requirements = await variableResolver.getInputRequirementsForElement(
1058+
elementRegistry.get('chainedInputMappings')
1059+
);
1060+
1061+
// then
1062+
const names = requirements.map(v => v.name);
1063+
expect(names).to.include('processVar3');
1064+
expect(names).to.not.include('localC');
1065+
expect(names).to.not.include('localD');
1066+
expect(requirements).to.have.length(1);
1067+
}));
1068+
1069+
1070+
it('should keep shadowed variable as requirement when mapping a to a', inject(async function(variableResolver, elementRegistry) {
1071+
1072+
// when
1073+
const requirements = await variableResolver.getInputRequirementsForElement(
1074+
elementRegistry.get('shadowingTask')
1075+
);
1076+
1077+
// then
1078+
const names = requirements.map(v => v.name);
1079+
expect(names).to.include('a');
1080+
expect(requirements).to.have.length(1);
1081+
}));
1082+
1083+
1084+
it('should handle shadowing with chained mappings', inject(async function(variableResolver, elementRegistry) {
1085+
1086+
// when
1087+
const requirements = await variableResolver.getInputRequirementsForElement(
1088+
elementRegistry.get('shadowingChainedTask')
1089+
);
1090+
1091+
// then
1092+
const names = requirements.map(v => v.name);
1093+
expect(names).to.include('a');
1094+
expect(names).to.not.include('b');
1095+
expect(requirements).to.have.length(1);
1096+
}));
1097+
10131098
});
10141099

10151100

0 commit comments

Comments
 (0)