libs/designer/src/lib/core/utils/tokens.ts (501 lines of code) (raw):

import Constants from '../../common/constants'; import type { NodeDataWithOperationMetadata } from '../actions/bjsworkflow/operationdeserializer'; import type { Settings } from '../actions/bjsworkflow/settings'; import type { WorkflowNode } from '../parsers/models/workflowNode'; import type { NodeOperation, OutputInfo } from '../state/operation/operationMetadataSlice'; import { updateRepetitionContext } from '../state/operation/operationMetadataSlice'; import type { TokensState } from '../state/tokens/tokensSlice'; import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; import type { WorkflowParameterDefinition, WorkflowParametersState } from '../state/workflowparameters/workflowparametersSlice'; import type { AppDispatch, RootState } from '../store'; import { getAllNodesInsideNode, getTriggerNodeId, getUpstreamNodeIds } from './graph'; import { addForeachToNode, getForeachActionName, getRepetitionContext, getRepetitionNodeIds, getTokenExpressionValueForManifestBasedOperation, shouldAddForeach, } from './loops'; import { removeAliasingKeyRedundancies } from './outputs'; import { ensureExpressionValue, FxBrandColor, FxIcon, getExpressionValueForOutputToken, getTokenValueFromToken, httpWebhookBrandColor, httpWebhookIcon, ParameterBrandColor, ParameterIcon, remapTokenSegmentValue, shouldIncludeSelfForRepetitionReference, } from './parameters/helper'; import { createTokenValueSegment } from './parameters/segment'; import { getSplitOnValue, hasSecureOutputs } from './setting'; import { getVariableTokens } from './variables'; import { OperationManifestService, getIntl, getKnownTitles, OutputKeys, labelCase, unmap, equals, filterRecord, getRecordEntry, TOKEN_PICKER_OUTPUT_SECTIONS, } from '@microsoft/logic-apps-shared'; import type { FunctionDefinition, OutputToken, Token, ValueSegment } from '@microsoft/designer-ui'; import { UIConstants, TemplateFunctions, TokenType } from '@microsoft/designer-ui'; import type { BuiltInOutput, OperationManifest } from '@microsoft/logic-apps-shared'; import { getAgentParameterTokens } from './agentParameters'; export interface TokenGroup { id: string; label: string; tokens: OutputToken[]; hasAdvanced?: boolean; showAdvanced?: boolean; } export const getTokenNodeIds = ( nodeId: string, graph: WorkflowNode, nodesMetadata: NodesMetadata, nodesManifest: Record<string, NodeDataWithOperationMetadata>, operationInfos: Record<string, NodeOperation>, operationMap: Record<string, string> ): string[] => { let tokenNodeIds = getUpstreamNodeIds(nodeId, graph, nodesMetadata, operationMap); const manifest = getRecordEntry(nodesManifest, nodeId)?.manifest; const preliminaryRepetitionNodeIds = getRepetitionNodeIds(nodeId, nodesMetadata, operationInfos); // Remove token nodes that have inaccessible outputs due to loop scope tokenNodeIds = tokenNodeIds.filter((tokenNodeId) => { const tokenRepititionNodes = getRepetitionNodeIds(tokenNodeId, nodesMetadata, operationInfos, { ignoreUntil: true }); // filter out if repetitionNodeIds does not contain all of tokenRepititionNodes return tokenRepititionNodes.every((tokenRepititionId) => preliminaryRepetitionNodeIds.includes(tokenRepititionId)); }); if (manifest) { // Should include itself as repetition reference if nodes can reference its outputs // generated by its inputs like Query, Select and Table operations. const includeSelf = shouldIncludeSelfForRepetitionReference(manifest); const repetitionNodeIds = getRepetitionNodeIds(nodeId, nodesMetadata, operationInfos, { includeSelf }); for (const repetitionNodeId of repetitionNodeIds) { const nodeManifest = getRecordEntry(nodesManifest, repetitionNodeId)?.manifest; // If repetition is set for a node but not set for self reference, // then nodes having this type as repetition can reference its outputs like Foreach and Until if (nodeManifest?.properties.repetition && !nodeManifest.properties.repetition.self) { tokenNodeIds.push(repetitionNodeId); } } if (manifest.properties?.outputTokens?.selfReference) { const allNodesInsideNode = getAllNodesInsideNode(nodeId, graph, operationMap); tokenNodeIds.push(...allNodesInsideNode); } } return Array.from(new Set(tokenNodeIds)); }; export const getBuiltInTokens = (manifest?: OperationManifest): OutputToken[] => { if (!manifest) { return []; } const icon = manifest.properties.iconUri; const brandColor = manifest.properties.brandColor; return (manifest.properties.outputTokens?.builtIns || []).map(({ name, title, required, type }: BuiltInOutput) => ({ key: `system.$.function.${name}`, brandColor, icon, title, name, type, isAdvanced: false, outputInfo: { type: TokenType.OUTPUTS, required, }, })); }; export const convertOutputsToTokens = ( nodeId: string | undefined, nodeType: string, outputs: Record<string, OutputInfo>, operationMetadata: { iconUri: string; brandColor: string }, settings?: Settings ): OutputToken[] => { const { iconUri: icon, brandColor } = operationMetadata; const isSecure = hasSecureOutputs(nodeType, settings); let tokenType: TokenType; switch (nodeType) { case Constants.NODE.TYPE.FOREACH: tokenType = TokenType.ITEM; break; case Constants.NODE.TYPE.AGENT_CONDITION: tokenType = TokenType.AGENTPARAMETER; break; default: tokenType = TokenType.OUTPUTS; } // TODO - Look at repetition context to get foreach context correctly in tokens and for splitOn return Object.keys(outputs).map((outputKey) => { const { key, name, type, isAdvanced, required, format, source, isInsideArray, isDynamic, parentArray, itemSchema, schema, value, alias, description, } = outputs[outputKey]; return { key: alias ? removeAliasingKeyRedundancies(key) : key, brandColor, icon, title: getTokenTitle(outputs[outputKey]), name, type, value, description, isAdvanced, outputInfo: { type: tokenType, required, format, source, isSecure, isDynamic, actionName: nodeId, arrayDetails: isInsideArray ? { itemSchema, parentArray } : undefined, schema, }, }; }); }; export const getExpressionTokenSections = (): TokenGroup[] => { return TemplateFunctions.map((functionGroup) => { const { id, name, functions } = functionGroup; const hasAdvanced = functions.some((func) => func.isAdvanced); const tokens = functions.map(({ name, defaultSignature, description, isAdvanced }: FunctionDefinition) => ({ key: name, brandColor: FxBrandColor, icon: FxIcon, title: defaultSignature, name, type: Constants.SWAGGER.TYPE.ANY, description, isAdvanced, outputInfo: { type: TokenType.FX, functionName: name, }, })); return { id, label: name, hasAdvanced, showAdvanced: false, tokens, }; }); }; export const getOutputTokenSections = ( nodeId: string, nodeType: string, tokenState: TokensState, workflowParametersState: WorkflowParametersState, workflowState: WorkflowState, replacementIds: Record<string, string>, includeCurrentNodeTokens = false ): TokenGroup[] => { const workflowParameters = filterRecord(workflowParametersState.definitions, (_, defintion) => defintion.name !== ''); const { variables, outputTokens, agentParameters } = tokenState; const nodeTokens = getRecordEntry(outputTokens, nodeId); const tokenGroups: TokenGroup[] = []; const intl = getIntl(); const agentParameterTokens = getAgentParameterTokens(nodeId, agentParameters, workflowState.nodesMetadata); if (agentParameterTokens?.length) { tokenGroups.push({ id: TOKEN_PICKER_OUTPUT_SECTIONS.AGENT_PARAMETERS, label: intl.formatMessage({ defaultMessage: 'Agent parameters', id: 'JWKCiG', description: 'Heading section for agent parameter tokens', }), tokens: agentParameterTokens, }); } if (Object.keys(workflowParameters).length) { tokenGroups.push({ id: TOKEN_PICKER_OUTPUT_SECTIONS.WORKFLOW_PARAMETERS, label: intl.formatMessage({ description: 'Heading section for Parameter tokens', defaultMessage: 'Parameters', id: 'J9wWry' }), tokens: getWorkflowParameterTokens(workflowParameters), }); } if (nodeTokens) { tokenGroups.push({ id: TOKEN_PICKER_OUTPUT_SECTIONS.VARIABLES, label: intl.formatMessage({ description: 'Heading section for Variable tokens', defaultMessage: 'Variables', id: 'unMaeV' }), tokens: getVariableTokens(variables, nodeTokens).map((token) => ({ ...token, value: getTokenValue(token, nodeType, replacementIds), })), }); if (nodeType.toLowerCase() === Constants.NODE.TYPE.HTTP_WEBHOOK) { tokenGroups.push(getListCallbackUrlToken(nodeId)); } const outputTokenGroups = nodeTokens.upstreamNodeIds.map((upstreamNodeId) => { let tokens = getRecordEntry(outputTokens, upstreamNodeId)?.tokens ?? []; tokens = tokens.map((token) => { return { ...token, value: getTokenValue(token, nodeType, replacementIds), }; }); if (!tokens.length) { return undefined; } return { id: upstreamNodeId, label: labelCase(getRecordEntry(replacementIds, upstreamNodeId) ?? upstreamNodeId), tokens, hasAdvanced: tokens.some((token) => token.isAdvanced), showAdvanced: false, }; }); if (includeCurrentNodeTokens) { let currentTokens = getRecordEntry(outputTokens, nodeId)?.tokens ?? []; currentTokens = currentTokens.map((token) => { return { ...token, value: getTokenValue(token, nodeType, replacementIds), }; }); if (currentTokens.length) { const currentTokensGroup = { id: nodeId, label: labelCase(getRecordEntry(replacementIds, nodeId) ?? nodeId), tokens: currentTokens, hasAdvanced: currentTokens.some((token) => token.isAdvanced), showAdvanced: false, }; outputTokenGroups.push(currentTokensGroup); } } tokenGroups.push(...(outputTokenGroups.filter((group) => !!group) as TokenGroup[])); } return tokenGroups; }; export const createValueSegmentFromToken = async ( nodeId: string, parameterId: string, token: OutputToken, addImplicitForeachIfNeeded: boolean, addLatestActionName: boolean, rootState: RootState, dispatch: AppDispatch ): Promise<ValueSegment> => { const tokenOwnerNodeId = token.outputInfo.actionName ?? getTriggerNodeId(rootState.workflow); const nodeType = getRecordEntry(rootState.operations.operationInfo, tokenOwnerNodeId)?.type ?? ''; const idReplacements = rootState.workflow.idReplacements; const tokenValueSegment = convertTokenToValueSegment(token, nodeType, idReplacements); if (addLatestActionName && tokenValueSegment.token?.actionName) { const newActionId = getRecordEntry(idReplacements, tokenValueSegment.token.actionName); if (newActionId && newActionId !== tokenValueSegment.token.actionName) { tokenValueSegment.token.actionName = newActionId; } } if (!addImplicitForeachIfNeeded) { return tokenValueSegment; } if (tokenValueSegment.token?.tokenType !== TokenType.PARAMETER && tokenValueSegment.token?.tokenType !== TokenType.VARIABLE) { const tokenOwnerActionName = token.outputInfo.actionName; const tokenOwnerOperationInfo = getRecordEntry(rootState.operations.operationInfo, tokenOwnerNodeId) as any; const { shouldAdd, arrayDetails, repetitionContext } = await shouldAddForeach(nodeId, parameterId, token, rootState); let newRootState = rootState; let newRepetitionContext = repetitionContext; if (shouldAdd) { const { payload: newState } = await dispatch(addForeachToNode({ arrayDetails, nodeId, token })); newRootState = newState as RootState; newRepetitionContext = await getRepetitionContext( nodeId, newRootState.operations.operationInfo, newRootState.operations.inputParameters, newRootState.workflow.nodesMetadata, /* includeSelf */ false, getSplitOnValue(newRootState.workflow, newRootState.operations), newRootState.workflow.idReplacements ); } if (newRepetitionContext) { dispatch(updateRepetitionContext({ id: nodeId, repetition: newRepetitionContext })); } if (arrayDetails?.length && newRepetitionContext) { (tokenValueSegment.token as Token).arrayDetails = { ...tokenValueSegment.token?.arrayDetails, loopSource: getForeachActionName(newRepetitionContext, arrayDetails[0].parentArrayKey, tokenOwnerActionName), }; } if (equals(tokenOwnerOperationInfo?.type, Constants.NODE.TYPE.FOREACH)) { (tokenValueSegment.token as Token).arrayDetails = { ...tokenValueSegment.token?.arrayDetails, loopSource: tokenOwnerActionName, }; } if (tokenValueSegment.token?.arrayDetails?.loopSource) { // NOTE: The foreach 'loopSource' may have been updated above. // Evaluate the expression value again to make sure the latest 'loopSource' is picked up. if (OperationManifestService().isSupported(tokenOwnerOperationInfo.type, tokenOwnerOperationInfo.kind)) { tokenValueSegment.value = getTokenExpressionValueForManifestBasedOperation( token.key, !!tokenValueSegment.token?.arrayDetails, tokenValueSegment.token?.arrayDetails?.loopSource, tokenOwnerActionName, !!tokenValueSegment.token?.required ); } else { ensureExpressionValue(tokenValueSegment, /* calculateValue */ true); } } if (tokenValueSegment.token) { tokenValueSegment.token.value = remapTokenSegmentValue(tokenValueSegment, rootState.workflow.idReplacements).value.value; } } return tokenValueSegment; }; const getTokenValueSegmentTokenType = (token: OutputToken, nodeType: string): TokenType => { const { key } = token; if (nodeType.toLowerCase() === Constants.NODE.TYPE.FOREACH) { return TokenType.ITEM; } if (key === Constants.UNTIL_CURRENT_ITERATION_INDEX_KEY) { return TokenType.ITERATIONINDEX; } if (token.outputInfo?.functionName) { if (token.outputInfo.functionName === Constants.FUNCTION_NAME.PARAMETERS) { return TokenType.PARAMETER; } if (token.outputInfo.functionName === Constants.FUNCTION_NAME.AGENT_PARAMETERS) { return TokenType.AGENTPARAMETER; } return TokenType.VARIABLE; } return TokenType.OUTPUTS; }; const convertTokenToValueSegment = (token: OutputToken, nodeType: string, replacementIds: Record<string, string>): ValueSegment => { const tokenType = getTokenValueSegmentTokenType(token, nodeType); const { key, brandColor, icon, title, description, name, type, outputInfo } = token; const { actionName, required, format, source, isSecure, arrayDetails, schema } = outputInfo; const segmentToken: Token = { key, name, type, title, brandColor, icon, description, tokenType, actionName, required, format, isSecure, source, arrayDetails: arrayDetails ? { parentArrayName: arrayDetails.parentArray, itemSchema: arrayDetails.itemSchema, loopSource: equals(nodeType, Constants.NODE.TYPE.UNTIL) ? actionName : undefined, } : undefined, schema, value: getTokenValue(token, nodeType, replacementIds), }; return createTokenValueSegment(segmentToken, segmentToken.value as string, format); }; export const getTokenTitle = (output: OutputInfo): string => { if (output.title) { return output.title; } const itemToken = getKnownTitles(OutputKeys.Item); if (output.isInsideArray) { return output.parentArray ? `${output.parentArray} - ${itemToken}` : itemToken; } return output.name ? getKnownTitles(output.name) : getKnownTitles(OutputKeys.Body); }; const getWorkflowParameterTokens = (definitions: Record<string, WorkflowParameterDefinition>): OutputToken[] => { return unmap(definitions).map(({ name, type }: WorkflowParameterDefinition) => { return { key: `parameters:${name}`, brandColor: ParameterBrandColor, icon: ParameterIcon, title: name, name, type: convertWorkflowParameterTypeToSwaggerType(type), isAdvanced: false, outputInfo: { type: TokenType.PARAMETER, functionName: Constants.FUNCTION_NAME.PARAMETERS, functionArguments: [name], }, value: getTokenValueFromToken(TokenType.PARAMETER, [name]), }; }); }; export const convertWorkflowParameterTypeToSwaggerType = (type: string | undefined): string => { switch (type?.toLowerCase()) { case UIConstants.WORKFLOW_PARAMETER_TYPE.FLOAT: return Constants.SWAGGER.TYPE.NUMBER; case UIConstants.WORKFLOW_PARAMETER_TYPE.INTEGER: case UIConstants.WORKFLOW_PARAMETER_TYPE.INT: return Constants.SWAGGER.TYPE.INTEGER; case UIConstants.WORKFLOW_PARAMETER_TYPE.BOOLEAN: case UIConstants.WORKFLOW_PARAMETER_TYPE.BOOL: return Constants.SWAGGER.TYPE.BOOLEAN; case UIConstants.WORKFLOW_PARAMETER_TYPE.STRING: return Constants.SWAGGER.TYPE.STRING; case UIConstants.WORKFLOW_PARAMETER_TYPE.ARRAY: return Constants.SWAGGER.TYPE.ARRAY; case UIConstants.WORKFLOW_PARAMETER_TYPE.OBJECT: return Constants.SWAGGER.TYPE.OBJECT; default: return Constants.SWAGGER.TYPE.ANY; } }; export const normalizeKey = (key: string): string => { return key.startsWith('outputs.$.body.') ? key.replace('outputs.$.body.', 'body.$.') : key; }; export const getTokenValue = (token: OutputToken, nodeType: string, replacementIds: Record<string, string>): string => { return rewriteValueId(token.outputInfo.actionName ?? '', getExpressionValueForOutputToken(token, nodeType) ?? '', replacementIds); }; const rewriteValueId = (id: string, value: string, replacementIds: Record<string, string>): string => { return value.replaceAll(id, getRecordEntry(replacementIds, id) ?? id); }; const getListCallbackUrlToken = (nodeId: string): TokenGroup => { const intl = getIntl(); const callbackUrlToken: OutputToken = { brandColor: httpWebhookBrandColor, key: Constants.HTTP_WEBHOOK_LIST_CALLBACK_URL_KEY, title: intl.formatMessage({ defaultMessage: 'Callback URL', id: 'hMpLz3', description: 'Callback url token title', }), type: Constants.SWAGGER.TYPE.STRING, icon: httpWebhookIcon, name: Constants.HTTP_WEBHOOK_LIST_CALLBACK_URL_NAME, value: Constants.HTTP_WEBHOOK_LIST_CALLBACK_URL_NAME, isAdvanced: false, outputInfo: { type: TokenType.OUTPUTS, required: false, isSecure: false, source: Constants.OUTPUTS, }, }; return { hasAdvanced: false, label: intl.formatMessage({ defaultMessage: 'Webhook reference information', id: 'FT3jt6', description: 'httpwebhook callback url section title', }), id: nodeId, tokens: [callbackUrlToken], }; };