Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Execute Workflow Node): Move Type Conversion functionality to ResourceMapper #12268

Merged
merged 15 commits into from
Dec 19, 2024
1 change: 1 addition & 0 deletions cypress/e2e/48-subworkflow-inputs.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ function validateAndReturnToParent(targetChild: string, offset: number, fields:
// Due to our workaround to remain in the same tab we need to select the correct tab manually
navigateWorkflowSelectionDropdown(offset, targetChild);

// This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I <think>
CharlieKolb marked this conversation as resolved.
Show resolved Hide resolved
ndv.actions.execute();

getOutputTableHeaders().should('have.length', fields.length + 1);
Expand Down
18 changes: 14 additions & 4 deletions cypress/fixtures/Test_Subworkflow-Inputs.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,23 @@
},
{
"parameters": {
"workflowId": {},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"ignoreTypeMismatchErrors": false,
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"id": "ddc82976-edd9-4488-a5a5-7f558a7d905b",
"name": "Execute Workflow",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [500, 240]
"typeVersion": 1.2,
"position": [500, 240],
"id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453",
"name": "Execute Workflow"
}
],
"connections": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { loadWorkflowInputMappings } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main reason for keeping these separate is for us to be able to customize them for the two nodes. Now realizing that they should behave the same and there is a bit of logic there to maintain to makes sense to unify it.

Copy link
Contributor Author

@CharlieKolb CharlieKolb Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I probably should have asked first, this just got caught up in the large merge conflict in general :D If we do modify behavior in the future it should probably still call a shared util in the end, so let's go with this 👍

import type {
INodeTypeBaseDescription,
ISupplyDataFunctions,
Expand All @@ -6,7 +7,6 @@ import type {
INodeTypeDescription,
} from 'n8n-workflow';

import { loadWorkflowInputMappings } from './methods/resourceMapping';
import { WorkflowToolService } from './utils/WorkflowToolService';
import { versionDescription } from './versionDescription';

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import get from 'lodash/get';
import isObject from 'lodash/isObject';
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
import type {
ExecuteWorkflowData,
ExecutionError,
Expand All @@ -22,7 +23,6 @@ import { z } from 'zod';

import type { FromAIArgument } from './FromAIParser';
import { AIParametersParser } from './FromAIParser';
import { getCurrentWorkflowInputData } from '../methods/resourceMapping';

/**
Main class for creating the Workflow tool
Expand Down
13 changes: 8 additions & 5 deletions packages/core/src/node-execution-context/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const validateResourceMapperValue = (
for (let i = 0; i < paramValueNames.length; i++) {
const key = paramValueNames[i];
const resolvedValue = paramValues[key];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const schemaEntry = schema.find((s) => s.id === key);

if (
Expand All @@ -99,15 +98,19 @@ const validateResourceMapperValue = (
};
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (schemaEntry?.type) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
valueOptions: schemaEntry.options,
strict: !resourceMapperField.attemptToConvertTypes,
parseStrings: !!resourceMapperField.convertFieldsToString,
});

if (!validationResult.valid) {
return { ...validationResult, fieldName: key };
if (!resourceMapperField.ignoreTypeMismatchErrors) {
return { ...validationResult, fieldName: key };
} else {
paramValues[key] = resolvedValue;
}
} else {
// If it's valid, set the casted value
paramValues[key] = validationResult.newValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
INodeParameters,
INodeProperties,
INodeTypeDescription,
NodeParameterValueType,
ResourceMapperField,
ResourceMapperValue,
} from 'n8n-workflow';
Expand Down Expand Up @@ -52,6 +53,12 @@ const state = reactive({
value: {},
matchingColumns: [] as string[],
schema: [] as ResourceMapperField[],
ignoreTypeMismatchErrors: false,
attemptToConvertTypes: false,
// This should always be true if `showTypeConversionOptions` is provided
// It's used to avoid accepting any value as string without casting it
// Which is the legacy behavior without these type options.
convertFieldsToString: false,
} as ResourceMapperValue,
parameterValues: {} as INodeParameters,
loading: false,
Expand Down Expand Up @@ -97,6 +104,10 @@ onMounted(async () => {
...state.parameterValues,
parameters: props.node.parameters,
};

if (showTypeConversionOptions.value) {
state.paramValue.convertFieldsToString = true;
}
}
const params = state.parameterValues.parameters as INodeParameters;
const parameterName = props.parameter.name;
Expand Down Expand Up @@ -161,6 +172,10 @@ const showMappingModeSelect = computed<boolean>(() => {
return props.parameter.typeOptions?.resourceMapper?.supportAutoMap !== false;
});

const showTypeConversionOptions = computed<boolean>(() => {
return props.parameter.typeOptions?.resourceMapper?.showTypeConversionOptions === true;
});

const showMatchingColumnsSelector = computed<boolean>(() => {
return (
!state.loading &&
Expand Down Expand Up @@ -572,5 +587,52 @@ defineExpose({
})
}}
</N8nNotice>
<div
v-if="showTypeConversionOptions && state.paramValue.schema.length > 0"
:class="$style.typeConversionOptions"
>
<ParameterInputFull
:parameter="{
name: 'attemptToConvertTypes',
type: 'boolean',
displayName: locale.baseText('resourceMapper.attemptToConvertTypes.displayName'),
default: false,
description: locale.baseText('resourceMapper.attemptToConvertTypes.description'),
}"
:path="props.path + '.attemptToConvertTypes'"
:value="state.paramValue.attemptToConvertTypes"
@update="
(x: IUpdateInformation<NodeParameterValueType>) => {
state.paramValue.attemptToConvertTypes = x.value as boolean;
emitValueChanged();
}
"
/>
<ParameterInputFull
:parameter="{
name: 'ignoreTypeMismatchErrors',
type: 'boolean',
displayName: locale.baseText('resourceMapper.ignoreTypeMismatchErrors.displayName'),
default: false,
description: locale.baseText('resourceMapper.ignoreTypeMismatchErrors.description'),
}"
:path="props.path + '.ignoreTypeMismatchErrors'"
:value="state.paramValue.ignoreTypeMismatchErrors"
@update="
(x: IUpdateInformation<NodeParameterValueType>) => {
state.paramValue.ignoreTypeMismatchErrors = x.value as boolean;
emitValueChanged();
}
"
/>
</div>
</div>
</template>

<style module lang="scss">
.typeConversionOptions {
CharlieKolb marked this conversation as resolved.
Show resolved Hide resolved
display: grid;
padding: var(--spacing-m);
gap: var(--spacing-2xs);
}
</style>
4 changes: 4 additions & 0 deletions packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,10 @@
"resourceMapper.addAllFields": "Add All {fieldWord}",
"resourceMapper.removeAllFields": "Remove All {fieldWord}",
"resourceMapper.refreshFieldList": "Refresh {fieldWord} List",
"resourceMapper.attemptToConvertTypes.displayName": "Attempt to convert types",
"resourceMapper.attemptToConvertTypes.description": "Attempt to convert types when mapping fields",
"resourceMapper.ignoreTypeMismatchErrors.displayName": "Ignore type mismatch errors",
"resourceMapper.ignoreTypeMismatchErrors.description": "Whether type mismatches should be ignored, rather than returning an Error",
"runData.openSubExecution": "Inspect Sub-Execution {id}",
"runData.openParentExecution": "Inspect Parent Execution {id}",
"runData.emptyItemHint": "This is an item, but it's empty.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,18 @@
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type {
ExecuteWorkflowData,
FieldValueOption,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
ResourceMapperField,
} from 'n8n-workflow';

import { getWorkflowInfo } from './GenericFunctions';
import { loadWorkflowInputMappings } from './methods/resourceMapping';
import { generatePairedItemData } from '../../../utils/utilities';
import { getWorkflowInputData } from '../../../utils/workflowInputsResourceMapping/GenericFunctions';

function getWorkflowInputValues(this: IExecuteFunctions) {
const inputData = this.getInputData();

return inputData.map((item, itemIndex) => {
const itemFieldValues = this.getNodeParameter(
'workflowInputs.value',
itemIndex,
{},
) as IDataObject;

return {
json: {
...item.json,
...itemFieldValues,
},
index: itemIndex,
pairedItem: {
item: itemIndex,
},
};
});
}

function getCurrentWorkflowInputData(this: IExecuteFunctions) {
const inputData = getWorkflowInputValues.call(this);

const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[];

if (schema.length === 0) {
return inputData;
} else {
const newParams = schema
.filter((x) => !x.removed)
.map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[];

return getWorkflowInputData.call(this, inputData, newParams);
}
}

import {
getCurrentWorkflowInputData,
loadWorkflowInputMappings,
} from '../../../utils/workflowInputsResourceMapping/GenericFunctions';
export class ExecuteWorkflow implements INodeType {
description: INodeTypeDescription = {
displayName: 'Execute Workflow',
Expand Down Expand Up @@ -84,6 +43,13 @@ export class ExecuteWorkflow implements INodeType {
},
],
},
{
displayName: 'This node is out of date. Please upgrade by removing it and adding a new one',
name: 'outdatedVersionWarning',
type: 'notice',
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
default: '',
},
{
displayName: 'Source',
name: 'source',
Expand Down Expand Up @@ -254,6 +220,7 @@ export class ExecuteWorkflow implements INodeType {
addAllFields: true,
multiKeyMatch: false,
supportAutoMap: false,
showTypeConversionOptions: true,
},
},
displayOptions: {
Expand Down
Loading
Loading