libs/designer/src/lib/core/utils/loops.ts (728 lines of code) (raw):
import Constants from '../../common/constants';
import { addTokensAndVariables } from '../actions/bjsworkflow/add';
import { updateAllUpstreamNodes } from '../actions/bjsworkflow/initialize';
import type { NodeDataWithOperationMetadata } from '../actions/bjsworkflow/operationdeserializer';
import { initializeOperationDetailsForManifest } from '../actions/bjsworkflow/operationdeserializer';
import { getOperationManifest } from '../queries/operation';
import type { NodeInputs, NodeOperation, NodeOutputs, OutputInfo } from '../state/operation/operationMetadataSlice';
import { initializeNodes } from '../state/operation/operationMetadataSlice';
import type { NodesMetadata, Operations } from '../state/workflow/workflowInterfaces';
import { addImplicitForeachNode } from '../state/workflow/workflowSlice';
import type { RootState } from '../store';
import { getAllParentsForNode, getNewNodeId, getTriggerNodeId } from './graph';
import type { RepetitionContext, RepetitionReference } from './parameters/helper';
import {
getTokenExpressionMethodFromKey,
generateExpressionFromKey,
getAllInputParameters,
updateTokenMetadata,
getParameterFromId,
getTokenExpressionValue,
getJSONValueFromString,
getParameterFromName,
parameterValueToString,
shouldIncludeSelfForRepetitionReference,
getCustomCodeFilesWithData,
shouldEncodeParameterValueForOperationBasedOnMetadata,
} from './parameters/helper';
import { isTokenValueSegment } from './parameters/segment';
import { TokenSegmentConvertor } from './parameters/tokensegment';
import { getSplitOnValue } from './setting';
import {
foreachOperationInfo,
OperationManifestService,
OutputKeys,
containsWildIndexSegment,
convertToStringLiteral,
isAncestorKey,
create,
createEx,
ExpressionParser,
ExpressionType,
isFunction,
isStringLiteral,
isTemplateExpression,
parseEx,
SegmentType,
clone,
equals,
first,
getRecordEntry,
isNullOrUndefined,
} from '@microsoft/logic-apps-shared';
import type { OutputToken, Token } from '@microsoft/designer-ui';
import { TokenType } from '@microsoft/designer-ui';
import type {
Dereference,
Expression,
ExpressionFunction,
ExpressionLiteral,
Segment,
OperationManifest,
} from '@microsoft/logic-apps-shared';
import { createAsyncThunk } from '@reduxjs/toolkit';
interface ImplicitForeachArrayDetails {
parentArrayKey: string;
parentArrayValue: string;
}
interface ImplicitForeachDetails {
shouldAdd: boolean;
arrayDetails?: ImplicitForeachArrayDetails[];
repetitionContext?: RepetitionContext;
}
export const shouldAddForeach = async (
nodeId: string,
parameterId: string,
token: OutputToken,
state: RootState
): Promise<ImplicitForeachDetails> => {
if (token.outputInfo.type === TokenType.VARIABLE || token.outputInfo.type === TokenType.PARAMETER || !token.outputInfo?.arrayDetails) {
return { shouldAdd: false };
}
const operationInfo = getRecordEntry(state.operations.operationInfo, nodeId);
if (!operationInfo) {
return { shouldAdd: false };
}
const inputParameters = getRecordEntry(state.operations.inputParameters, nodeId) ?? { parameterGroups: {} };
const parameter = getParameterFromId(inputParameters, parameterId);
const manifest = OperationManifestService().isSupported(operationInfo.type, operationInfo.kind)
? await getOperationManifest(operationInfo)
: undefined;
const includeSelf = manifest ? shouldIncludeSelfForRepetitionReference(manifest, parameter?.parameterName) : false;
const repetitionContext = await getRepetitionContext(
nodeId,
state.operations.operationInfo,
state.operations.inputParameters,
state.workflow.nodesMetadata,
includeSelf,
getSplitOnValue(state.workflow, state.operations),
state.workflow.idReplacements
);
const tokenOwnerNodeId = token.outputInfo.actionName ?? getTriggerNodeId(state.workflow);
const tokenOwnerOperationInfo = state.operations.operationInfo[tokenOwnerNodeId];
const areOutputsManifestBased = OperationManifestService().isSupported(tokenOwnerOperationInfo.type, tokenOwnerOperationInfo.kind);
const { shouldAdd, arrayDetails } = getArrayDetailsForNestedForeach(
token,
tokenOwnerNodeId,
areOutputsManifestBased,
repetitionContext,
state
);
return { shouldAdd, arrayDetails, repetitionContext };
};
export const addForeachToNode = createAsyncThunk(
'addForeachToNode',
async (
payload: { nodeId: string; arrayDetails: ImplicitForeachArrayDetails[] | undefined; token: OutputToken },
{ dispatch, getState }
): Promise<RootState> => {
const { nodeId, arrayDetails, token } = payload;
if (!arrayDetails?.length) {
throw new Error('The value for foreach property should not be empty');
}
const state = getState() as RootState;
const splitOn = getSplitOnValue(state.workflow, state.operations);
let manifest: OperationManifest | undefined;
const foreachNodeIds: string[] = [];
let currentNodeId = nodeId;
for (const arrayDetail of arrayDetails) {
const { parentArrayValue: arrayName } = arrayDetail;
// Adding foreach node.
const foreachNodeId = getNewNodeId((getState() as RootState).workflow, 'For_each');
foreachNodeIds.push(foreachNodeId);
dispatch(
addImplicitForeachNode({
nodeId: currentNodeId,
foreachNodeId,
operation: { type: Constants.NODE.TYPE.FOREACH, foreach: arrayName },
})
);
currentNodeId = foreachNodeId;
}
for (let i = foreachNodeIds.length - 1; i >= 0; i--) {
const newState = getState() as RootState;
const foreachNodeId = foreachNodeIds[i];
// Initializing details for newly added foreach operation.
const foreachOperation = newState.workflow.operations[foreachNodeId];
const customCodeWithData = getCustomCodeFilesWithData(state.customCode);
const [{ nodeInputs, nodeOutputs, nodeDependencies, settings }] = (await initializeOperationDetailsForManifest(
foreachNodeId,
foreachOperation,
customCodeWithData,
/* isTrigger */ false,
state.workflow.workflowKind,
dispatch
)) as NodeDataWithOperationMetadata[];
const repetitionInfo = await getRepetitionContext(
foreachNodeId,
newState.operations.operationInfo,
{ ...newState.operations.inputParameters, [foreachNodeId]: nodeInputs },
newState.workflow.nodesMetadata,
/* includeSelf */ false,
splitOn,
newState.workflow.idReplacements
);
updateTokenMetadataInForeachInputs(
nodeInputs,
token,
/* loopSource */ i < foreachNodeIds.length - 1 ? foreachNodeIds[i + 1] : undefined,
repetitionInfo,
newState
);
if (!manifest) {
manifest = await getOperationManifest(foreachOperationInfo);
}
const { iconUri, brandColor } = manifest.properties;
const initData = {
id: foreachNodeId,
nodeInputs,
nodeOutputs,
nodeDependencies,
settings,
operationMetadata: { iconUri, brandColor },
repetitionInfo,
};
dispatch(initializeNodes({ nodes: [initData] }));
addTokensAndVariables(foreachNodeId, Constants.NODE.TYPE.FOREACH, { ...initData, manifest }, newState, dispatch);
}
updateAllUpstreamNodes(getState() as RootState, dispatch);
return getState() as RootState;
}
);
interface GetRepetitionNodeIdsOptions {
includeSelf?: boolean;
ignoreUntil?: boolean;
}
export const getRepetitionNodeIds = (
nodeId: string,
nodesMetadata: NodesMetadata,
operationInfos: Record<string, NodeOperation>,
{ includeSelf = false, ignoreUntil = false }: GetRepetitionNodeIdsOptions = {}
): string[] => {
const allParentNodeIds = getAllParentsForNode(nodeId, nodesMetadata);
const repetitionNodeIds = allParentNodeIds.filter((parentId) => isLoopingNode(parentId, operationInfos, ignoreUntil));
if (includeSelf) {
repetitionNodeIds.unshift(nodeId);
}
return repetitionNodeIds;
};
export const getRepetitionContext = async (
nodeId: string,
operationInfos: Record<string, NodeOperation>,
allInputs: Record<string, NodeInputs>,
nodesMetadata: NodesMetadata,
includeSelf: boolean,
splitOn: string | undefined,
idReplacements?: Record<string, string>
): Promise<RepetitionContext> => {
const repetitionNodeIds = getRepetitionNodeIds(nodeId, nodesMetadata, operationInfos, { includeSelf });
const repetitionReferences = (
await Promise.all(
repetitionNodeIds.map((repetitionNodeId) => getRepetitionReference(repetitionNodeId, operationInfos, allInputs, idReplacements))
)
).filter((reference) => !!reference) as RepetitionReference[];
const parentReferences: RepetitionReference[] = [];
for (let i = repetitionReferences.length - 1; i >= 0; i--) {
const repetitionReference = repetitionReferences[i];
if (
typeof repetitionReference.repetitionValue === 'string' &&
repetitionReference.actionType !== Constants.NODE.TYPE.UNTIL &&
isTemplateExpression(repetitionReference.repetitionValue)
) {
const foreach = parseForeach(repetitionReference.repetitionValue, {
splitOn,
repetitionReferences: parentReferences,
});
repetitionReference.repetitionPath = foreach.fullPath;
repetitionReference.repetitionStep = foreach.step;
}
parentReferences.unshift(repetitionReference);
}
// TODO: Might need to update the aliased values for repetition references for open api.
return {
repetitionReferences,
splitOn,
};
};
const getArrayDetailsForNestedForeach = (
token: OutputToken,
tokenOwnerNodeId: string,
areOutputsManifestBased: boolean,
repetitionContext: RepetitionContext,
state: RootState
): ImplicitForeachDetails => {
let shouldAdd = false;
const arrayDetails: ImplicitForeachArrayDetails[] = [];
const actionName = token.outputInfo.actionName;
let parentArrayKey = getParentArrayKey(token.key);
let parentArray = token.outputInfo.arrayDetails?.parentArray;
while (parentArrayKey !== undefined) {
const data = getParentArrayExpression(
actionName,
parentArrayKey,
parentArray,
repetitionContext,
state.operations.outputParameters[tokenOwnerNodeId],
areOutputsManifestBased
);
const isSplitOn = isExpressionEqualToNodeSplitOn(
data?.expression as string,
state.operations.settings[tokenOwnerNodeId].splitOn?.value?.enabled
? state.operations.settings[tokenOwnerNodeId].splitOn?.value?.value
: undefined
);
// eslint-disable-next-line no-loop-func
const alreadyInLoop = repetitionContext.repetitionReferences.some((repetitionReference) => {
const { repetitionValue } = repetitionReference;
if (typeof repetitionValue !== 'string') {
return false;
}
return checkArrayInRepetition(actionName, repetitionValue, parentArrayKey, data?.expression, data?.output, areOutputsManifestBased);
});
const shouldAddLoopForCurrentParent = !isSplitOn && !alreadyInLoop;
if (shouldAddLoopForCurrentParent && data?.expression) {
arrayDetails.push({ parentArrayKey, parentArrayValue: data.expression });
}
shouldAdd = shouldAdd || shouldAddLoopForCurrentParent;
parentArrayKey = data?.token.arrayDetails ? getParentArrayKey(parentArrayKey) : undefined;
parentArray = data?.token.arrayDetails?.parentArrayName;
}
return { shouldAdd, arrayDetails };
};
const getParentArrayExpression = (
tokenOwnerActionName: string | undefined,
parentArrayKey: string | undefined,
parentArrayName: string | undefined,
repetitionContext: RepetitionContext,
nodeOutputs: NodeOutputs,
areOutputsManifestBased: boolean
): { expression: string; output: OutputInfo; token: Token } | undefined => {
if (!parentArrayKey) {
return undefined;
}
const { repetitionReferences } = repetitionContext;
const sanitizedParentArrayKey = sanitizeKey(parentArrayKey);
const parentArrayOutput = nodeOutputs.outputs[parentArrayKey];
const parentArrayKeyOfParentArray = getParentArrayKey(parentArrayKey);
const parentArrayTokenInfo: Token = {
actionName: tokenOwnerActionName,
key: parentArrayKey,
name: parentArrayName,
tokenType: TokenType.OUTPUTS,
title: parentArrayName as string,
};
if (parentArrayOutput) {
parentArrayTokenInfo.required = parentArrayOutput.required;
parentArrayTokenInfo.name = parentArrayOutput.name;
parentArrayTokenInfo.title = parentArrayOutput.title;
parentArrayTokenInfo.source = parentArrayOutput.source;
if (parentArrayOutput.isInsideArray) {
parentArrayTokenInfo.arrayDetails = {
parentArrayKey: parentArrayKeyOfParentArray ? parentArrayKeyOfParentArray : undefined,
parentArrayName: parentArrayOutput.parentArray ? parentArrayOutput.parentArray : undefined,
};
}
} else {
parentArrayTokenInfo.arrayDetails = parentArrayKeyOfParentArray ? { parentArrayKey: parentArrayKeyOfParentArray } : undefined;
}
for (const repetitionReference of repetitionReferences) {
if (
equals(sanitizedParentArrayKey, repetitionReference.repetitionPath) &&
((isNullOrUndefined(tokenOwnerActionName) && isNullOrUndefined(repetitionReference.repetitionStep)) ||
equals(tokenOwnerActionName, repetitionReference.repetitionStep))
) {
return { expression: repetitionReference.repetitionValue, output: parentArrayOutput, token: parentArrayTokenInfo };
}
if (
isAncestorKey(sanitizedParentArrayKey, repetitionReference.repetitionPath) &&
((isNullOrUndefined(tokenOwnerActionName) && isNullOrUndefined(repetitionReference.repetitionStep)) ||
equals(tokenOwnerActionName, repetitionReference.repetitionStep))
) {
const extraSegments = getExtraSegments(parentArrayKey, repetitionReference.repetitionPath);
// NOTE: if after the foreach repetition path between parent array path, it still contains one index segment
// we need to treat it as items('parentLoopSource') if inside foreach, or item() if in other repetition supporting nodes
if (extraSegments.filter((segment) => segment.type === SegmentType.Index).length === 1) {
parentArrayTokenInfo.arrayDetails = {
loopSource: equals(repetitionReference.actionType, Constants.NODE.TYPE.FOREACH) ? repetitionReference.actionName : undefined,
};
}
}
}
return {
expression: areOutputsManifestBased
? `@${getTokenExpressionValueForManifestBasedOperation(
parentArrayTokenInfo.key,
!!parentArrayTokenInfo.arrayDetails,
parentArrayTokenInfo.arrayDetails?.loopSource,
tokenOwnerActionName,
!!parentArrayTokenInfo.required
)}`
: `@${getTokenExpressionValue(parentArrayTokenInfo)}`,
output: parentArrayOutput,
token: parentArrayTokenInfo,
};
};
const checkArrayInRepetition = (
actionName: string | undefined,
repetitionValue: string,
tokenKey: string | undefined,
tokenValue: string | undefined,
outputInfo: OutputInfo | undefined,
areOutputsManifestBased: boolean
): boolean => {
if (equals(repetitionValue, tokenValue)) {
return true;
}
if (areOutputsManifestBased && tokenKey) {
const method = getTokenExpressionMethodFromKey(tokenKey, actionName, outputInfo?.source);
const sanitizedValue = `@${generateExpressionFromKey(
method,
tokenKey,
actionName,
!!outputInfo?.isInsideArray,
!!outputInfo?.required
)}`;
return equals(repetitionValue, sanitizedValue);
}
return false;
};
// Directly checking the node type, because cannot make async calls while adding token from picker to editor.
// TODO - See if this can be made async and looked at manifest.
export const isLoopingNode = (nodeId: string, operationInfos: Record<string, NodeOperation>, ignoreUntil: boolean): boolean => {
const nodeType = getRecordEntry(operationInfos, nodeId)?.type;
return equals(nodeType, Constants.NODE.TYPE.FOREACH) || (!ignoreUntil && equals(nodeType, Constants.NODE.TYPE.UNTIL));
};
export const getForeachActionName = (
context: RepetitionContext,
foreachExpressionPath: string,
repetitionStep: string | undefined
): string | undefined => {
const sanitizedPath = sanitizeKey(foreachExpressionPath);
const foreachAction = first(
(item) =>
equals(item.actionType, Constants.NODE.TYPE.FOREACH) &&
equals(normalizeKeyPath(item.repetitionPath), normalizeKeyPath(sanitizedPath)) &&
item.repetitionStep === repetitionStep,
context.repetitionReferences
);
return foreachAction?.actionName;
};
export const isForeachActionNameForLoopsource = (
nodeId: string,
expression: string,
nodes: Record<string, Partial<NodeDataWithOperationMetadata>>,
operations: Operations,
nodesMetadata: NodesMetadata
): boolean => {
const operationInfos: Record<string, NodeOperation> = {};
for (const operationId of Object.keys(operations)) {
operationInfos[operationId] = {
type: operations[operationId]?.type,
kind: operations[operationId]?.kind,
connectorId: '',
operationId: '',
};
}
const repetitionNodeIds = getRepetitionNodeIds(nodeId, nodesMetadata, operationInfos);
const sanitizedPath = sanitizeKey(expression);
const foreachAction = first((item) => {
const operation = operationInfos[item];
const parameter = nodes[item]?.nodeInputs ? getParameterFromName(nodes[item].nodeInputs as NodeInputs, 'foreach') : undefined;
const foreachValue =
operation && (operation as any).foreach
? (operation as any).foreach
: parameter && parameter.value.length === 1 && parameter.value[0].token?.key;
return equals(operation?.type, Constants.NODE.TYPE.FOREACH) && equals(foreachValue, sanitizedPath);
}, repetitionNodeIds);
return !!foreachAction;
};
const getRepetitionReference = async (
nodeId: string,
operationInfos: Record<string, NodeOperation>,
allInputs: Record<string, NodeInputs>,
idReplacements?: Record<string, string>
): Promise<RepetitionReference | undefined> => {
const operationInfo = getRecordEntry(operationInfos, nodeId);
if (!operationInfo) {
return undefined;
}
const service = OperationManifestService();
if (service.isSupported(operationInfo.type, operationInfo.kind)) {
const manifest = await getOperationManifest(operationInfo);
const parameterName = manifest.properties.repetition?.loopParameter;
const nodeInputs = getRecordEntry(allInputs, nodeId) ?? { parameterGroups: {} };
const parameter = parameterName ? getParameterFromName(nodeInputs, parameterName) : undefined;
if (parameter) {
const repetitionValue = getJSONValueFromString(
parameterValueToString(
parameter,
/* isDefinitionValue */ true,
idReplacements,
shouldEncodeParameterValueForOperationBasedOnMetadata(operationInfo)
),
parameter.type
);
return {
actionName: nodeId,
actionType: operationInfo.type,
repetitionValue,
};
}
}
return undefined;
};
interface Foreach {
step?: string;
path?: string;
fullPath?: string;
}
export const parseForeach = (repetitionValue: string, repetitionContext: RepetitionContext): Foreach => {
const foreach: Foreach = {};
if (repetitionValue) {
let foreachExpression: Expression | undefined;
let splitOnExpression: Expression | undefined;
try {
foreachExpression = ExpressionParser.parseTemplateExpression(repetitionValue);
} catch {
// for invalid case, we just ignore it and return empty object
}
if (repetitionContext.splitOn) {
try {
splitOnExpression = ExpressionParser.parseTemplateExpression(repetitionContext.splitOn);
} catch {
// for invalid case, we just ignore it
}
}
if (foreachExpression) {
switch (foreachExpression.type) {
case ExpressionType.Function: {
const functionExpression = foreachExpression as ExpressionFunction;
if (TokenSegmentConvertor.isOutputToken(functionExpression) || TokenSegmentConvertor.isVariableToken(functionExpression)) {
foreach.fullPath = getFullPath(functionExpression, splitOnExpression);
if (functionExpression.dereferences.length > 0) {
foreach.path = buildSegmentPath(functionExpression.dereferences);
} else {
foreach.path = create([OutputKeys.Body]);
}
if (functionExpression.arguments.length > 0) {
const argumentExpression = functionExpression.arguments[0] as ExpressionLiteral;
foreach.step = argumentExpression.value;
}
} else if (TokenSegmentConvertor.isItemToken(functionExpression) || TokenSegmentConvertor.isItemsToken(functionExpression)) {
let parentRepetitionValue: any;
let validRepetitionReferences: RepetitionReference[] = [];
if (TokenSegmentConvertor.isItemToken(functionExpression)) {
validRepetitionReferences = [...repetitionContext.repetitionReferences];
validRepetitionReferences.shift();
parentRepetitionValue = getClosestRepetitionValue(repetitionContext);
} else {
const actionExpression = functionExpression.arguments[0] as ExpressionLiteral;
const parentForeachName = actionExpression.value;
parentRepetitionValue = getRepetitionValue(repetitionContext, parentForeachName);
if (parentRepetitionValue) {
let shouldAdd = false;
for (const reference of repetitionContext.repetitionReferences) {
if (shouldAdd) {
validRepetitionReferences.push(reference);
}
if (equals(reference.actionName, parentForeachName)) {
shouldAdd = true;
}
}
}
}
if (typeof parentRepetitionValue === 'string' && isTemplateExpression(parentRepetitionValue)) {
const parentRepetitionContext: RepetitionContext = {
splitOn: repetitionContext.splitOn,
repetitionReferences: validRepetitionReferences,
};
const parentForeach = parseForeach(parentRepetitionValue, parentRepetitionContext);
const parentSegments = parseEx(parentForeach.fullPath);
for (const item of functionExpression.dereferences) {
const literalExpression = item.expression as ExpressionLiteral;
parentSegments.push({
type: SegmentType.Property,
value: literalExpression.value,
});
}
foreach.step = parentForeach.step;
foreach.path = buildSegmentPath(functionExpression.dereferences);
foreach.fullPath = createEx(parentSegments);
}
}
break;
}
default:
break;
}
}
}
return foreach;
};
const getClosestRepetitionValue = (repetitionContext: RepetitionContext): any => {
return repetitionContext?.repetitionReferences?.at(0)?.repetitionValue;
};
const getRepetitionValue = (repetitionContext: RepetitionContext, actionName?: string): any => {
return getRepetitionReferenceFromContext(repetitionContext, actionName)?.repetitionValue;
};
const getRepetitionReferenceFromContext = (repetitionContext: RepetitionContext, actionName?: string): RepetitionReference | undefined => {
if (actionName) {
return first((item) => equals(item.actionName, actionName), repetitionContext.repetitionReferences);
}
return repetitionContext?.repetitionReferences?.at(0);
};
const isExpressionEqualToNodeSplitOn = (test: string | any[], splitOn: string | undefined): boolean => {
if (typeof test !== 'string') {
return false;
}
if (splitOn === undefined || test === undefined) {
return false;
}
if (equals(sanitizeOperatorsInExpression(test), sanitizeOperatorsInExpression(splitOn))) {
return true;
}
return false;
};
export const getTokenExpressionValueForManifestBasedOperation = (
key: string,
isInsideArray: boolean,
loopSource: string | undefined,
actionName: string | undefined,
required: boolean
): string => {
// TODO: This might require update for Open API for aliasing support.
const method = isInsideArray
? loopSource
? `items(${convertToStringLiteral(loopSource)})`
: Constants.ITEM
: actionName
? `${Constants.OUTPUTS}(${convertToStringLiteral(actionName)})`
: Constants.TRIGGER_OUTPUTS_OUTPUT;
return generateExpressionFromKey(method, key, actionName, isInsideArray, required, /* overrideMethod */ false);
};
/**
* generate the full path for the specifiecd expression segments, e.g, for @body('action')?[test] => body.$.test
* if the splitOn is not empty, and the expression is triggerBody(), then it would also append the splitOn path,
* e.g, @triggerBody()?[attachments] with splitOn: @triggerBody()['value'], the result would be: body.$.value.attachments
*/
const getFullPath = (expressionSegment: ExpressionFunction, splitOn?: Expression): string => {
const segments = [equals(expressionSegment.name, 'outputs') ? 'outputs' : 'body', '$'];
// handle the splitOn first
// TODO: log the issue if splitOn is not functionExpression
if (equals(expressionSegment.name, 'triggerbody') && !!splitOn) {
if (isFunction(splitOn)) {
if (equals(splitOn.name, expressionSegment.name)) {
for (const dereference of splitOn.dereferences) {
if (isStringLiteral(dereference.expression)) {
segments.push(dereference.expression.value);
}
}
}
}
}
// TODO: log the issue if expression is not string
for (const dereference of expressionSegment.dereferences) {
if (isStringLiteral(dereference.expression)) {
segments.push(dereference.expression.value);
}
}
return create(segments) as string;
};
const buildSegmentPath = (dereferences: Dereference[]): string => {
return dereferences.map((dereference) => `['${(dereference.expression as ExpressionLiteral).value}']`).join('');
};
export const getParentArrayKey = (key: string): string | undefined => {
const segments = parseEx(key);
if (segments.length > 1) {
for (let index = segments.length - 1; index >= 0; index--) {
if (segments[index].type === SegmentType.Index) {
return createEx(segments.slice(0, index));
}
}
}
return undefined;
};
const sanitizeKey = (original: string): string => {
if (original) {
if (containsWildIndexSegment(original)) {
const segments = parseEx(original);
const sanitizedSegments: Segment[] = [];
for (const segment of segments) {
if (segment.type !== SegmentType.Index) {
sanitizedSegments.push(segment);
}
}
return createEx(sanitizedSegments) as string;
}
}
return original;
};
const getExtraSegments = (key: string, ancestorKey: string | undefined): Segment[] => {
let childSegments: Segment[] = [];
let startIndex = 0;
if (key && ancestorKey) {
childSegments = parseEx(key);
const ancestorSegments = parseEx(ancestorKey);
let ancestorStartIndex = 0;
if (ancestorSegments.length < childSegments.length) {
for (startIndex = 0; startIndex < childSegments.length; startIndex++) {
const childSegment = childSegments[startIndex];
const ancestorSegment = ancestorSegments[ancestorStartIndex];
if (childSegment.type === SegmentType.Property && childSegment.value === ancestorSegment.value) {
ancestorStartIndex++;
}
if (ancestorStartIndex === ancestorSegments.length) {
startIndex++;
break;
}
}
}
}
return childSegments.slice(startIndex);
};
// NOTE: The implicit addition of Foreach node was governed by adding a token, so we will be only using that
// tokenOwnerNodeId to populate the info and loopSource for adding correct loopSource in multiple foreach additions.
// This method should only be used for Implicit Foreach node addition.
const updateTokenMetadataInForeachInputs = (
inputs: NodeInputs,
token: OutputToken,
loopSource: string | undefined,
repetitionContext: RepetitionContext,
rootState: RootState
): void => {
const allParameters = getAllInputParameters(inputs);
const triggerNodeId = getTriggerNodeId(rootState.workflow);
const tokenOwnerNodeId = token.outputInfo.actionName ?? triggerNodeId;
const actionNodes = { [tokenOwnerNodeId]: tokenOwnerNodeId };
const nodesData = {
[tokenOwnerNodeId]: {
settings: rootState.operations.settings[tokenOwnerNodeId],
nodeOutputs: rootState.operations.outputParameters[tokenOwnerNodeId],
operationMetadata: { brandColor: token.brandColor, iconUri: token.icon },
},
} as Record<string, NodeDataWithOperationMetadata>;
for (const parameter of allParameters) {
const segments = parameter.value;
if (segments && segments.length) {
parameter.value = segments.map((segment) => {
if (isTokenValueSegment(segment)) {
const updatedSegment = clone(segment);
(updatedSegment.token as Token).actionName = token.outputInfo.actionName;
(updatedSegment.token as Token).arrayDetails = updatedSegment.token?.arrayDetails
? { ...updatedSegment.token.arrayDetails, loopSource }
: undefined;
const finalSegment = updateTokenMetadata(
updatedSegment,
repetitionContext,
actionNodes,
triggerNodeId,
nodesData,
{ [tokenOwnerNodeId]: rootState.workflow.operations[tokenOwnerNodeId] },
rootState.workflowParameters.definitions,
rootState.workflow.nodesMetadata,
parameter.type
);
if (loopSource) {
finalSegment.value = getTokenExpressionValueForManifestBasedOperation(
finalSegment.token?.key as string,
!!finalSegment.token?.arrayDetails,
finalSegment.token?.arrayDetails?.loopSource,
finalSegment.token?.actionName,
!!finalSegment.token?.required
);
}
return finalSegment;
}
return segment;
});
}
}
};
/**
* This method normalizes the parameter or output key path to contain body segments because repetition
* references always have body segments. This should only be used while finding out foreach reference.
*/
const normalizeKeyPath = (path: string | undefined): string | undefined => {
return path && path.startsWith('outputs.$.body.')
? path.replace('outputs.$.body.', 'body.$.')
: path === 'outputs.$.body'
? 'body.$'
: path;
};
const sanitizeOperatorsInExpression = (expression: string): string => {
return expression.replaceAll('?[', '[');
};