mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(Set Node): Overhaul (#6348)
Github issue / Community forum post (link here to close automatically): https://github.com/n8n-io/n8n/pull/6348 --------- Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
@@ -7,6 +7,7 @@ const commonDescription: INodeProperties = {
|
||||
typeOptions: {
|
||||
editor: 'codeNodeEditor',
|
||||
editorLanguage: 'javaScript',
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
|
||||
@@ -7,6 +7,7 @@ const commonDescription: INodeProperties = {
|
||||
typeOptions: {
|
||||
editor: 'codeNodeEditor',
|
||||
editorLanguage: 'python',
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
|
||||
@@ -76,6 +76,7 @@ export class CrateDb implements INodeType {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
|
||||
@@ -20,6 +20,7 @@ const properties: INodeProperties[] = [
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
@@ -38,6 +39,7 @@ const properties: INodeProperties[] = [
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
||||
@@ -11,8 +11,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { chunk, flatten, getResolvables } from '@utils/utilities';
|
||||
|
||||
import mssql from 'mssql';
|
||||
|
||||
import type { ITables } from './TableInterface';
|
||||
@@ -27,6 +25,7 @@ import {
|
||||
extractValues,
|
||||
formatColumns,
|
||||
} from './GenericFunctions';
|
||||
import { chunk, flatten, getResolvables } from '@utils/utilities';
|
||||
|
||||
export class MicrosoftSql implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -93,6 +92,7 @@ export class MicrosoftSql implements INodeType {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'MSSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
|
||||
@@ -78,6 +78,7 @@ const versionDescription: INodeTypeDescription = {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'MySQL',
|
||||
},
|
||||
displayOptions: {
|
||||
|
||||
@@ -8,11 +8,10 @@ import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import type { QueryRunner, QueryWithValues } from '../../helpers/interfaces';
|
||||
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { prepareQueryAndReplacements, replaceEmptyStringsByNulls } from '../../helpers/utils';
|
||||
|
||||
import { optionsCollection } from '../common.descriptions';
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
@@ -27,6 +26,7 @@ const properties: INodeProperties[] = [
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'MySQL',
|
||||
},
|
||||
hint: 'Consider using query parameters to prevent SQL injection attacks. Add them in the options below',
|
||||
|
||||
@@ -77,6 +77,7 @@ const versionDescription: INodeTypeDescription = {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
|
||||
@@ -6,13 +6,12 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type { PgpDatabase, QueriesRunner, QueryWithValues } from '../../helpers/interfaces';
|
||||
|
||||
import { replaceEmptyStringsByNulls } from '../../helpers/utils';
|
||||
|
||||
import { optionsCollection } from '../common.descriptions';
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
@@ -27,6 +26,7 @@ const properties: INodeProperties[] = [
|
||||
"The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.",
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
hint: 'Consider using query parameters to prevent SQL injection attacks. Add them in the options below',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { configurePostgres } from '../transport';
|
||||
import { configureQueryRunner } from '../helpers/utils';
|
||||
import type { PostgresType } from './node.type';
|
||||
|
||||
import * as database from './database/Database.resource';
|
||||
import { configurePostgres } from '../transport';
|
||||
import { configureQueryRunner } from '../helpers/utils';
|
||||
|
||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
let returnData: INodeExecutionData[] = [];
|
||||
|
||||
@@ -63,6 +63,7 @@ export class QuestDb implements INodeType {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["JSON", "Filter", "Transform", "Map"],
|
||||
"alias": ["JSON", "Filter", "Transform", "Map", "Set"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Data Transformation"]
|
||||
}
|
||||
|
||||
@@ -1,217 +1,26 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import set from 'lodash/set';
|
||||
import { SetV1 } from './v1/SetV1.node';
|
||||
import { SetV2 } from './v2/SetV2.node';
|
||||
|
||||
export class Set implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
icon: 'fa:pen',
|
||||
group: ['input'],
|
||||
version: [1, 2],
|
||||
description: 'Sets values on items and optionally remove other values',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Keep Only Set',
|
||||
name: 'keepOnlySet',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether only the values set on this node should be kept and all others removed',
|
||||
},
|
||||
{
|
||||
displayName: 'Values to Set',
|
||||
name: 'values',
|
||||
placeholder: 'Add Value',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
description: 'The value to set',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
requiresDataPath: 'single',
|
||||
default: 'propertyName',
|
||||
description:
|
||||
'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description: 'The boolean value to write in the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
requiresDataPath: 'single',
|
||||
description:
|
||||
'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The number value to write in the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
requiresDataPath: 'single',
|
||||
description:
|
||||
'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The string value to write in the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
export class Set extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
icon: 'fa:pen',
|
||||
group: ['input'],
|
||||
description: 'Add or edit fields on an input item and optionally remove other fields',
|
||||
defaultVersion: 3,
|
||||
};
|
||||
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Dot Notation',
|
||||
name: 'dotNotation',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description:
|
||||
'<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.<p></p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new SetV1(baseDescription),
|
||||
2: new SetV1(baseDescription),
|
||||
3: new SetV2(baseDescription),
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({ json: {} });
|
||||
}
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let keepOnlySet: boolean;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean;
|
||||
item = items[itemIndex];
|
||||
const options = this.getNodeParameter('options', itemIndex, {});
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
pairedItem: item.pairedItem,
|
||||
};
|
||||
|
||||
if (!keepOnlySet) {
|
||||
if (item.binary !== undefined) {
|
||||
// Create a shallow copy of the binary data so that the old
|
||||
// data references which do not get changed still stay behind
|
||||
// but the incoming data does not get changed.
|
||||
newItem.binary = {};
|
||||
Object.assign(newItem.binary, item.binary);
|
||||
}
|
||||
|
||||
newItem.json = deepCopy(item.json);
|
||||
}
|
||||
|
||||
// Add boolean values
|
||||
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = !!setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, !!setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add number values
|
||||
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (
|
||||
nodeVersion >= 2 &&
|
||||
typeof setItem.value === 'string' &&
|
||||
!Number.isNaN(Number(setItem.value))
|
||||
) {
|
||||
setItem.value = Number(setItem.value);
|
||||
}
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add string values
|
||||
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
returnData.push(newItem);
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
||||
656
packages/nodes-base/nodes/Set/test/Set.v3.workflow.json
Normal file
656
packages/nodes-base/nodes/Set/test/Set.v3.workflow.json
Normal file
@@ -0,0 +1,656 @@
|
||||
{
|
||||
"name": "My workflow 22",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "fbb0f637-5a91-4227-af0a-cde04cd6059d",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-460,
|
||||
980
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {\n \"string\": {\n test2: \"hello\",\n test3: \" \",\n test4: \"\",\n test5: \"3\",\n test6: \"3,14\",\n test7: \"3.14\",\n test8: \"false\",\n test8: \"TRUE\",\n test9: \"false\",\n test10: \"1\",\n test11: '[\"apples\", \"oranges\"]',\n test12: '\"apples\", \"oranges\"',\n test13: '[1, 2]',\n test14: '{\"a\": 1, \"b\": { \"c\": 10, \"d\": \"test\"}}',\n test15: '{\"a\": 1}',\n test16: \"null\",\n test17: \"undefined\",\n test18: \"0\",\n },\n \"number\": {\n test1: 52472,\n test2: -1,\n test3: 0,\n test4: 1.334535,\n test5: null,\n test6: undefined,\n test7: 1,\n },\n \"boolean\": {\n // test1: 1,\n // test2: 0,\n test3: true,\n test4: false,\n },\n \"date\": {\n test1: $now,\n test2: \"2023-08-01T12:34:56Z\",\n test3: \"2016-05-25T09:24:15.123\",\n test4: \"Tue, 01 Nov 2016 13:23:12 +0630\",\n test5: \"2017-05-15 09:24:15\",\n test6: \"1542674993\",\n test7: 1542674993,\n },\n \"array\": {\n test13: [1,2,3,4],\n },\n \"object\": {\n obj: {\n objKey: 2,\n objArray: [1,2,3,4],\n objBool: true\n }\n },\n }\n];"
|
||||
},
|
||||
"id": "15a372ee-5243-409f-b28e-3eb3ec211e38",
|
||||
"name": "Code1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-200,
|
||||
980
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "numberToString1",
|
||||
"stringValue": "={{ $json.number.test1 }}"
|
||||
},
|
||||
{
|
||||
"name": "numberToString2",
|
||||
"stringValue": "={{ $json.number.test2 }}"
|
||||
},
|
||||
{
|
||||
"name": "numberToString3",
|
||||
"stringValue": "={{ $json.number.test4 }}"
|
||||
},
|
||||
{
|
||||
"name": "boolToString1",
|
||||
"stringValue": "={{ $json.boolean.test3 }}"
|
||||
},
|
||||
{
|
||||
"name": "boolToString2",
|
||||
"stringValue": "={{ $json.boolean.test4 }}"
|
||||
},
|
||||
{
|
||||
"name": "arrayToString1",
|
||||
"stringValue": "={{ $json.array.test13 }}"
|
||||
},
|
||||
{
|
||||
"name": "objectToString1",
|
||||
"stringValue": "={{ $json.object.obj }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "570b8f0e-1153-40a5-984f-5c1ae370fc0b",
|
||||
"name": "To String",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
160,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToNumber1",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.string.test5 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToNumber2",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.string.test7 }}"
|
||||
},
|
||||
{
|
||||
"name": "boolToNumber1",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.boolean.test3 }}"
|
||||
},
|
||||
{
|
||||
"name": "boolToNumber2",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.boolean.test4 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "660214cb-d38f-4566-b91e-98f3407f7348",
|
||||
"name": "To Number",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
160,
|
||||
800
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToBoolean1",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test8 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean3",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test9 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean4",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test10 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean5",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test18 }}"
|
||||
},
|
||||
{
|
||||
"name": "numberToBoolean1",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.number.test3 }}"
|
||||
},
|
||||
{
|
||||
"name": "numberToBoolean2",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.number.test7 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "f3e5b73a-6b55-4822-8864-25e9a73a3fe7",
|
||||
"name": "To Boolean",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
160,
|
||||
980
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToArray1",
|
||||
"type": "arrayValue",
|
||||
"arrayValue": "={{ $json.string.test11 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToArray2",
|
||||
"type": "arrayValue",
|
||||
"arrayValue": "={{ $json.string.test13 }}"
|
||||
},
|
||||
{
|
||||
"name": "arrayToArray1",
|
||||
"type": "arrayValue",
|
||||
"arrayValue": "={{ $json.array.test13 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "1385e092-ad14-4be7-8e3b-3645a56e0e22",
|
||||
"name": "To Array",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
160,
|
||||
1180
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToObject1",
|
||||
"type": "objectValue",
|
||||
"objectValue": "={{ $json.string.test14 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToObject2",
|
||||
"type": "objectValue",
|
||||
"objectValue": "={{ $json.string.test15 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "df394ded-3519-4604-859d-a50cbc788a56",
|
||||
"name": "To Object",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
160,
|
||||
1360
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "### Strict type checking",
|
||||
"height": 1063.125,
|
||||
"width": 369.6875
|
||||
},
|
||||
"id": "442560f9-6e05-467a-bdb4-02c8c8313e77",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
80,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "### Loose type checking",
|
||||
"height": 1058.046875,
|
||||
"width": 310.703125
|
||||
},
|
||||
"id": "b2ff8103-5d4c-46ec-9ca7-d7db8e4b3789",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
560,
|
||||
544.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToNumber1",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.string.test2 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToNumber2",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.string.test3 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToNumber3",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.string.test9 }}"
|
||||
},
|
||||
{
|
||||
"name": "arrayToNumber1",
|
||||
"type": "numberValue",
|
||||
"numberValue": "={{ $json.array.test13 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "ff5d7e99-4c8d-48e6-bfbf-70b32e3e19d9",
|
||||
"name": "To Number1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
600,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToBoolean1",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test5 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean2",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "=3,14"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean3",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test7 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean4",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test11 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean5",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test12 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToBoolean6",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.string.test17 }}"
|
||||
},
|
||||
{
|
||||
"name": "numberToBoolean1",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.number.test1 }}"
|
||||
},
|
||||
{
|
||||
"name": "numberToBoolean2",
|
||||
"type": "booleanValue",
|
||||
"booleanValue": "={{ $json.number.test4 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "7ec129f4-ac9d-4cff-b1a8-c957a8119a07",
|
||||
"name": "To Boolean1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
600,
|
||||
800
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToArray1",
|
||||
"type": "arrayValue",
|
||||
"arrayValue": "={{ $json.string.test2 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToArray2",
|
||||
"type": "arrayValue",
|
||||
"arrayValue": "={{ $json.string.test5 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "7b33fa7e-1cc8-44af-b251-f2521df25618",
|
||||
"name": "To Array1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
600,
|
||||
980
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "stringToObject1",
|
||||
"type": "objectValue",
|
||||
"objectValue": "={{ $json.string.test14 }}"
|
||||
},
|
||||
{
|
||||
"name": "stringToObject2",
|
||||
"type": "objectValue",
|
||||
"objectValue": "={{ $json.string.test15 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "030066a3-7c27-45fc-9173-22866f977fea",
|
||||
"name": "To Object1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
600,
|
||||
1180
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "b19c8f55-836f-43c8-9eed-30f51e99d150",
|
||||
"name": "No Operation, do nothing",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "8bacf8f8-314f-4be1-a090-667792000cb4",
|
||||
"name": "No Operation, do nothing1",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
360,
|
||||
200
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"To String": [
|
||||
{
|
||||
"json": {
|
||||
"numberToString1": "52472",
|
||||
"numberToString2": "-1",
|
||||
"numberToString3": "1.334535",
|
||||
"boolToString1": "true",
|
||||
"boolToString2": "false",
|
||||
"arrayToString1": "[1,2,3,4]",
|
||||
"objectToString1": "{\"objKey\":2,\"objArray\":[1,2,3,4],\"objBool\":true}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Number": [
|
||||
{
|
||||
"json": {
|
||||
"stringToNumber1": 3,
|
||||
"stringToNumber2": 3.14,
|
||||
"boolToNumber1": 1,
|
||||
"boolToNumber2": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Boolean": [
|
||||
{
|
||||
"json": {
|
||||
"stringToBoolean1": true,
|
||||
"stringToBoolean3": false,
|
||||
"stringToBoolean4": true,
|
||||
"stringToBoolean5": false,
|
||||
"numberToBoolean1": false,
|
||||
"numberToBoolean2": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Array": [
|
||||
{
|
||||
"json": {
|
||||
"stringToArray1": [
|
||||
"apples",
|
||||
"oranges"
|
||||
],
|
||||
"stringToArray2": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"arrayToArray1": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Object": [
|
||||
{
|
||||
"json": {
|
||||
"stringToObject1": {
|
||||
"a": 1,
|
||||
"b": {
|
||||
"c": 10,
|
||||
"d": "test"
|
||||
}
|
||||
},
|
||||
"stringToObject2": {
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Number1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToNumber1": "hello",
|
||||
"stringToNumber2": 0,
|
||||
"stringToNumber3": "false",
|
||||
"arrayToNumber1": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Boolean1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToBoolean1": "3",
|
||||
"stringToBoolean2": "3,14",
|
||||
"stringToBoolean3": "3.14",
|
||||
"stringToBoolean4": "[\"apples\", \"oranges\"]",
|
||||
"stringToBoolean5": "\"apples\", \"oranges\"",
|
||||
"stringToBoolean6": "undefined",
|
||||
"numberToBoolean1": 52472,
|
||||
"numberToBoolean2": 1.334535
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Object1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToObject1": {
|
||||
"a": 1,
|
||||
"b": {
|
||||
"c": 10,
|
||||
"d": "test"
|
||||
}
|
||||
},
|
||||
"stringToObject2": {
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Array1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToArray1": "hello",
|
||||
"stringToArray2": "3"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "To String",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Number",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Boolean",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Array",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Object",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "No Operation, do nothing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"No Operation, do nothing": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"No Operation, do nothing1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "To Number1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Boolean1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Array1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Object1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "b54f2d8f-158d-4540-839f-37c0bda20d9b",
|
||||
"id": "yVUBwSyuyegX6JIL",
|
||||
"meta": {
|
||||
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
247
packages/nodes-base/nodes/Set/test/v2/utils.test.ts
Normal file
247
packages/nodes-base/nodes/Set/test/v2/utils.test.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
|
||||
import { constructExecutionMetaData } from 'n8n-core';
|
||||
import get from 'lodash/get';
|
||||
import { composeReturnItem, parseJsonParameter, validateEntry } from '../../v2/helpers/utils';
|
||||
import type { SetNodeOptions } from '../../v2/helpers/interfaces';
|
||||
|
||||
export const node: INode = {
|
||||
id: '11',
|
||||
name: 'Edit Fields',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
position: [42, 42],
|
||||
parameters: {
|
||||
mode: 'manual',
|
||||
fields: {
|
||||
values: [],
|
||||
},
|
||||
include: 'none',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const createMockExecuteFunction = (nodeParameters: IDataObject) => {
|
||||
const fakeExecuteFunction = {
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
_itemIndex: number,
|
||||
fallbackValue?: IDataObject | undefined,
|
||||
options?: IGetNodeParameterOptions | undefined,
|
||||
) {
|
||||
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
|
||||
return get(nodeParameters, parameter, fallbackValue);
|
||||
},
|
||||
getNode() {
|
||||
return node;
|
||||
},
|
||||
helpers: { constructExecutionMetaData },
|
||||
continueOnFail: () => false,
|
||||
} as unknown as IExecuteFunctions;
|
||||
return fakeExecuteFunction;
|
||||
};
|
||||
|
||||
describe('test Set2, composeReturnItem', () => {
|
||||
it('should compose return item including no other fields', () => {
|
||||
const fakeExecuteFunction = createMockExecuteFunction({});
|
||||
|
||||
const inputItem = {
|
||||
json: {
|
||||
input1: 'value1',
|
||||
input2: 2,
|
||||
input3: [1, 2, 3],
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const newData = {
|
||||
num1: 55,
|
||||
str1: '42',
|
||||
arr1: ['foo', 'bar'],
|
||||
obj: {
|
||||
key: 'value',
|
||||
},
|
||||
};
|
||||
|
||||
const options: SetNodeOptions = {
|
||||
include: 'none',
|
||||
};
|
||||
|
||||
const result = composeReturnItem.call(fakeExecuteFunction, 0, inputItem, newData, options);
|
||||
|
||||
expect(result).toEqual({
|
||||
json: {
|
||||
num1: 55,
|
||||
str1: '42',
|
||||
arr1: ['foo', 'bar'],
|
||||
obj: {
|
||||
key: 'value',
|
||||
},
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should compose return item including selected fields', () => {
|
||||
const fakeExecuteFunction = createMockExecuteFunction({ includeFields: 'input1, input2' });
|
||||
|
||||
const inputItem = {
|
||||
json: {
|
||||
input1: 'value1',
|
||||
input2: 2,
|
||||
input3: [1, 2, 3],
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const newData = {
|
||||
num1: 55,
|
||||
str1: '42',
|
||||
arr1: ['foo', 'bar'],
|
||||
obj: {
|
||||
key: 'value',
|
||||
},
|
||||
};
|
||||
|
||||
const options: SetNodeOptions = {
|
||||
include: 'selected',
|
||||
};
|
||||
|
||||
const result = composeReturnItem.call(fakeExecuteFunction, 0, inputItem, newData, options);
|
||||
|
||||
expect(result).toEqual({
|
||||
json: {
|
||||
num1: 55,
|
||||
str1: '42',
|
||||
arr1: ['foo', 'bar'],
|
||||
input1: 'value1',
|
||||
input2: 2,
|
||||
obj: {
|
||||
key: 'value',
|
||||
},
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('test Set2, parseJsonParameter', () => {
|
||||
it('should parse valid JSON string', () => {
|
||||
const result = parseJsonParameter('{"foo": "bar"}', node, 0, 'test');
|
||||
|
||||
expect(result).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('should tolerate single quotes in string', () => {
|
||||
const result = parseJsonParameter("{'foo': 'bar'}", node, 0, 'test');
|
||||
|
||||
expect(result).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('should tolerate unquoted keys', () => {
|
||||
const result = parseJsonParameter("{foo: 'bar'}", node, 0, 'test');
|
||||
|
||||
expect(result).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('should tolerate trailing comma', () => {
|
||||
const result = parseJsonParameter('{"foo": "bar"},', node, 0, 'test');
|
||||
|
||||
expect(result).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('should tolerate trailing commas in objects', () => {
|
||||
const result = parseJsonParameter("{foo: 'bar', baz: {'foo': 'bar',}, }", node, 0, 'test');
|
||||
|
||||
expect(result).toEqual({
|
||||
foo: 'bar',
|
||||
baz: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('test Set2, validateEntry', () => {
|
||||
it('should convert number to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: 42 as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
value: '42',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert array to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: [1, 2, 3] as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
value: '[1,2,3]',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert object to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: { foo: 'bar' } as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
value: '{"foo":"bar"}',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert boolean to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: true as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
value: 'true',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert undefined to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: undefined as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
value: 'undefined',
|
||||
});
|
||||
});
|
||||
});
|
||||
227
packages/nodes-base/nodes/Set/v1/SetV1.node.ts
Normal file
227
packages/nodes-base/nodes/Set/v1/SetV1.node.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
|
||||
import set from 'lodash/set';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
icon: 'fa:pen',
|
||||
group: ['input'],
|
||||
version: [1, 2],
|
||||
description: 'Sets values on items and optionally remove other values',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Keep Only Set',
|
||||
name: 'keepOnlySet',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether only the values set on this node should be kept and all others removed',
|
||||
},
|
||||
{
|
||||
displayName: 'Values to Set',
|
||||
name: 'values',
|
||||
placeholder: 'Add Value',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
description: 'The value to set',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
requiresDataPath: 'single',
|
||||
default: 'propertyName',
|
||||
description:
|
||||
'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description: 'The boolean value to write in the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
requiresDataPath: 'single',
|
||||
description:
|
||||
'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The number value to write in the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
requiresDataPath: 'single',
|
||||
description:
|
||||
'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The string value to write in the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Dot Notation',
|
||||
name: 'dotNotation',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description:
|
||||
'<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.<p></p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export class SetV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const items = this.getInputData();
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({ json: {} });
|
||||
}
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let keepOnlySet: boolean;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean;
|
||||
item = items[itemIndex];
|
||||
const options = this.getNodeParameter('options', itemIndex, {});
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
pairedItem: item.pairedItem,
|
||||
};
|
||||
|
||||
if (!keepOnlySet) {
|
||||
if (item.binary !== undefined) {
|
||||
// Create a shallow copy of the binary data so that the old
|
||||
// data references which do not get changed still stay behind
|
||||
// but the incoming data does not get changed.
|
||||
newItem.binary = {};
|
||||
Object.assign(newItem.binary, item.binary);
|
||||
}
|
||||
|
||||
newItem.json = deepCopy(item.json);
|
||||
}
|
||||
|
||||
// Add boolean values
|
||||
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = !!setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, !!setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add number values
|
||||
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (
|
||||
nodeVersion >= 2 &&
|
||||
typeof setItem.value === 'string' &&
|
||||
!Number.isNaN(Number(setItem.value))
|
||||
) {
|
||||
setItem.value = Number(setItem.value);
|
||||
}
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add string values
|
||||
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
returnData.push(newItem);
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
262
packages/nodes-base/nodes/Set/v2/SetV2.node.ts
Normal file
262
packages/nodes-base/nodes/Set/v2/SetV2.node.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { IncludeMods, SetField, SetNodeOptions } from './helpers/interfaces';
|
||||
import { INCLUDE } from './helpers/interfaces';
|
||||
|
||||
import * as raw from './raw.mode';
|
||||
import * as manual from './manual.mode';
|
||||
|
||||
type Mode = 'manual' | 'raw';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Edit Fields (Set)',
|
||||
name: 'set',
|
||||
icon: 'fa:pen',
|
||||
group: ['input'],
|
||||
version: 3,
|
||||
description: 'Change the structure of your items',
|
||||
subtitle: '={{$parameter["mode"]}}',
|
||||
defaults: {
|
||||
name: 'Edit Fields',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Manual Mapping',
|
||||
value: 'manual',
|
||||
description: 'Edit item fields one by one',
|
||||
action: 'Edit item fields one by one',
|
||||
},
|
||||
{
|
||||
name: 'JSON Output',
|
||||
value: 'raw',
|
||||
description: 'Customize item output with JSON',
|
||||
action: 'Customize item output with JSON',
|
||||
},
|
||||
],
|
||||
default: 'manual',
|
||||
},
|
||||
{
|
||||
displayName: 'Duplicate Item',
|
||||
name: 'duplicateItem',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
isNodeSetting: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Duplicate Item Count',
|
||||
name: 'duplicateCount',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
},
|
||||
description:
|
||||
'How many times the item should be duplicated, mainly used for testing and debugging',
|
||||
isNodeSetting: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
duplicateItem: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Item duplication is set in the node settings. This option will be ignored when the workflow runs automatically.',
|
||||
name: 'duplicateWarning',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
duplicateItem: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
...raw.description,
|
||||
...manual.description,
|
||||
{
|
||||
displayName: 'Include in Output',
|
||||
name: 'include',
|
||||
type: 'options',
|
||||
description: 'How to select the fields you want to include in your output items',
|
||||
default: 'all',
|
||||
options: [
|
||||
{
|
||||
name: 'All Input Fields',
|
||||
value: INCLUDE.ALL,
|
||||
description: 'Also include all unchanged fields from the input',
|
||||
},
|
||||
{
|
||||
name: 'No Input Fields',
|
||||
value: INCLUDE.NONE,
|
||||
description: 'Include only the fields specified above',
|
||||
},
|
||||
{
|
||||
name: 'Selected Input Fields',
|
||||
value: INCLUDE.SELECTED,
|
||||
description: 'Also include the fields listed in the parameter “Fields to Include”',
|
||||
},
|
||||
{
|
||||
name: 'All Input Fields Except',
|
||||
value: INCLUDE.EXCEPT,
|
||||
description: 'Exclude the fields listed in the parameter “Fields to Exclude”',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Fields to Include',
|
||||
name: 'includeFields',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. fieldToInclude1,fieldToInclude2',
|
||||
description:
|
||||
'Comma-separated list of the field names you want to include in the output. You can drag the selected fields from the input panel.',
|
||||
requiresDataPath: 'multiple',
|
||||
displayOptions: {
|
||||
show: {
|
||||
include: ['selected'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Fields to Exclude',
|
||||
name: 'excludeFields',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. fieldToExclude1,fieldToExclude2',
|
||||
description:
|
||||
'Comma-separated list of the field names you want to exclude from the output. You can drag the selected fields from the input panel.',
|
||||
requiresDataPath: 'multiple',
|
||||
displayOptions: {
|
||||
show: {
|
||||
include: ['except'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Binary Data',
|
||||
name: 'includeBinary',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether binary data should be included if present in the input item',
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore Type Conversion Errors',
|
||||
name: 'ignoreConversionErrors',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to ignore field type errors and apply a less strict type conversion',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/mode': ['manual'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Support Dot Notation',
|
||||
name: 'dotNotation',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description:
|
||||
'By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }. If that is not intended this can be deactivated, it will then set { "a.b": value } instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export class SetV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const items = this.getInputData();
|
||||
const mode = this.getNodeParameter('mode', 0) as Mode;
|
||||
const duplicateItem = this.getNodeParameter('duplicateItem', 0, false) as boolean;
|
||||
|
||||
const setNode = { raw, manual };
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const rawData: IDataObject = {};
|
||||
|
||||
if (mode === 'raw') {
|
||||
const jsonOutput = this.getNodeParameter('jsonOutput', 0, '', {
|
||||
rawExpressions: true,
|
||||
}) as string | undefined;
|
||||
|
||||
if (jsonOutput?.startsWith('=')) {
|
||||
rawData.jsonOutput = jsonOutput.replace(/^=+/, '');
|
||||
}
|
||||
} else {
|
||||
const workflowFieldsJson = this.getNodeParameter('fields.values', 0, [], {
|
||||
rawExpressions: true,
|
||||
}) as SetField[];
|
||||
|
||||
for (const entry of workflowFieldsJson) {
|
||||
if (entry.type === 'objectValue' && (entry.objectValue as string).startsWith('=')) {
|
||||
rawData[entry.name] = (entry.objectValue as string).replace(/^=+/, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const include = this.getNodeParameter('include', i) as IncludeMods;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const node = this.getNode();
|
||||
|
||||
options.include = include;
|
||||
|
||||
const newItem = await setNode[mode].execute.call(
|
||||
this,
|
||||
items[i],
|
||||
i,
|
||||
options as SetNodeOptions,
|
||||
rawData,
|
||||
node,
|
||||
);
|
||||
|
||||
if (duplicateItem && this.getMode() === 'manual') {
|
||||
const duplicateCount = this.getNodeParameter('duplicateCount', 0, 0) as number;
|
||||
for (let j = 0; j <= duplicateCount; j++) {
|
||||
returnData.push(newItem);
|
||||
}
|
||||
} else {
|
||||
returnData.push(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
27
packages/nodes-base/nodes/Set/v2/helpers/interfaces.ts
Normal file
27
packages/nodes-base/nodes/Set/v2/helpers/interfaces.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
export type SetNodeOptions = {
|
||||
dotNotation?: boolean;
|
||||
ignoreConversionErrors?: boolean;
|
||||
include?: IncludeMods;
|
||||
includeBinary?: boolean;
|
||||
};
|
||||
|
||||
export type SetField = {
|
||||
name: string;
|
||||
type: 'stringValue' | 'numberValue' | 'booleanValue' | 'arrayValue' | 'objectValue';
|
||||
stringValue?: string;
|
||||
numberValue?: number;
|
||||
booleanValue?: boolean;
|
||||
arrayValue?: string[] | string | IDataObject | IDataObject[];
|
||||
objectValue?: string | IDataObject;
|
||||
};
|
||||
|
||||
export const INCLUDE = {
|
||||
ALL: 'all',
|
||||
NONE: 'none',
|
||||
SELECTED: 'selected',
|
||||
EXCEPT: 'except',
|
||||
} as const;
|
||||
|
||||
export type IncludeMods = (typeof INCLUDE)[keyof typeof INCLUDE];
|
||||
211
packages/nodes-base/nodes/Set/v2/helpers/utils.ts
Normal file
211
packages/nodes-base/nodes/Set/v2/helpers/utils.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type {
|
||||
FieldType,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy, NodeOperationError, jsonParse, validateFieldType } from 'n8n-workflow';
|
||||
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
import unset from 'lodash/unset';
|
||||
|
||||
import type { SetNodeOptions, SetField } from './interfaces';
|
||||
import { INCLUDE } from './interfaces';
|
||||
import { getResolvables } from '../../../../utils/utilities';
|
||||
|
||||
const configureFieldHelper = (dotNotation?: boolean) => {
|
||||
if (dotNotation !== false) {
|
||||
return {
|
||||
set: (item: IDataObject, key: string, value: IDataObject) => {
|
||||
set(item, key, value);
|
||||
},
|
||||
get: (item: IDataObject, key: string) => {
|
||||
return get(item, key);
|
||||
},
|
||||
unset: (item: IDataObject, key: string) => {
|
||||
unset(item, key);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
set: (item: IDataObject, key: string, value: IDataObject) => {
|
||||
item[key] = value;
|
||||
},
|
||||
get: (item: IDataObject, key: string) => {
|
||||
return item[key];
|
||||
},
|
||||
unset: (item: IDataObject, key: string) => {
|
||||
delete item[key];
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function composeReturnItem(
|
||||
this: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
inputItem: INodeExecutionData,
|
||||
newFields: IDataObject,
|
||||
options: SetNodeOptions,
|
||||
) {
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
pairedItem: inputItem.pairedItem,
|
||||
};
|
||||
|
||||
if (options.includeBinary && inputItem.binary !== undefined) {
|
||||
// Create a shallow copy of the binary data so that the old
|
||||
// data references which do not get changed still stay behind
|
||||
// but the incoming data does not get changed.
|
||||
newItem.binary = {};
|
||||
Object.assign(newItem.binary, inputItem.binary);
|
||||
}
|
||||
|
||||
const fieldHelper = configureFieldHelper(options.dotNotation);
|
||||
|
||||
switch (options.include) {
|
||||
case INCLUDE.ALL:
|
||||
newItem.json = deepCopy(inputItem.json);
|
||||
break;
|
||||
case INCLUDE.SELECTED:
|
||||
const includeFields = (this.getNodeParameter('includeFields', itemIndex) as string)
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item);
|
||||
|
||||
for (const key of includeFields) {
|
||||
const fieldValue = fieldHelper.get(inputItem.json, key) as IDataObject;
|
||||
let keyToSet = key;
|
||||
if (options.dotNotation !== false && key.includes('.')) {
|
||||
keyToSet = key.split('.').pop() as string;
|
||||
}
|
||||
fieldHelper.set(newItem.json, keyToSet, fieldValue);
|
||||
}
|
||||
break;
|
||||
case INCLUDE.EXCEPT:
|
||||
const excludeFields = (this.getNodeParameter('excludeFields', itemIndex) as string)
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item);
|
||||
|
||||
const inputData = deepCopy(inputItem.json);
|
||||
|
||||
for (const key of excludeFields) {
|
||||
fieldHelper.unset(inputData, key);
|
||||
}
|
||||
|
||||
newItem.json = inputData;
|
||||
break;
|
||||
case INCLUDE.NONE:
|
||||
break;
|
||||
default:
|
||||
throw new Error(`The include option "${options.include}" is not known!`);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(newFields)) {
|
||||
fieldHelper.set(newItem.json, key, newFields[key] as IDataObject);
|
||||
}
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
export const parseJsonParameter = (
|
||||
jsonData: string | IDataObject,
|
||||
node: INode,
|
||||
i: number,
|
||||
entryName?: string,
|
||||
) => {
|
||||
let returnData: IDataObject;
|
||||
const location = entryName ? `entry "${entryName}" inside 'Fields to Set'` : "'JSON Output'";
|
||||
|
||||
if (typeof jsonData === 'string') {
|
||||
try {
|
||||
returnData = jsonParse<IDataObject>(jsonData);
|
||||
} catch (error) {
|
||||
let recoveredData = '';
|
||||
try {
|
||||
recoveredData = jsonData
|
||||
.replace(/'/g, '"') // Replace single quotes with double quotes
|
||||
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2":') // Wrap keys in double quotes
|
||||
.replace(/,\s*([\]}])/g, '$1') // Remove trailing commas from objects
|
||||
.replace(/,+$/, ''); // Remove trailing comma
|
||||
returnData = jsonParse<IDataObject>(recoveredData);
|
||||
} catch (err) {
|
||||
const description =
|
||||
recoveredData === jsonData ? jsonData : `${recoveredData};\n Original input: ${jsonData}`;
|
||||
throw new NodeOperationError(node, `The ${location} in item ${i} contains invalid JSON`, {
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
returnData = jsonData;
|
||||
}
|
||||
|
||||
if (returnData === undefined || typeof returnData !== 'object' || Array.isArray(returnData)) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`The ${location} in item ${i} does not contain a valid JSON object`,
|
||||
);
|
||||
}
|
||||
|
||||
return returnData;
|
||||
};
|
||||
|
||||
export const validateEntry = (
|
||||
entry: SetField,
|
||||
node: INode,
|
||||
itemIndex: number,
|
||||
ignoreErrors = false,
|
||||
) => {
|
||||
let entryValue = entry[entry.type];
|
||||
const name = entry.name;
|
||||
const entryType = entry.type.replace('Value', '') as FieldType;
|
||||
|
||||
if (entryType === 'string') {
|
||||
if (typeof entryValue === 'object') {
|
||||
entryValue = JSON.stringify(entryValue);
|
||||
} else {
|
||||
entryValue = String(entryValue);
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = validateFieldType(name, entryValue, entryType);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
if (ignoreErrors) {
|
||||
validationResult.newValue = entry[entry.type];
|
||||
} else {
|
||||
const message = `${validationResult.errorMessage} [item ${itemIndex}]`;
|
||||
const description = `To fix the error try to change the type for the field "${name}" or activate the option “Ignore Type Conversion Errors” to apply a less strict type validation`;
|
||||
throw new NodeOperationError(node, message, {
|
||||
itemIndex,
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const value = validationResult.newValue === undefined ? null : validationResult.newValue;
|
||||
|
||||
return { name, value };
|
||||
};
|
||||
|
||||
export function resolveRawData(this: IExecuteFunctions, rawData: string, i: number) {
|
||||
const resolvables = getResolvables(rawData);
|
||||
let returnData: string = rawData;
|
||||
|
||||
if (resolvables.length) {
|
||||
for (const resolvable of resolvables) {
|
||||
const resolvedValue = this.evaluateExpression(`${resolvable}`, i);
|
||||
|
||||
if (typeof resolvedValue === 'object' && resolvedValue !== null) {
|
||||
returnData = returnData.replace(resolvable, JSON.stringify(resolvedValue));
|
||||
} else {
|
||||
returnData = returnData.replace(resolvable, resolvedValue as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
208
packages/nodes-base/nodes/Set/v2/manual.mode.ts
Normal file
208
packages/nodes-base/nodes/Set/v2/manual.mode.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
parseJsonParameter,
|
||||
validateEntry,
|
||||
composeReturnItem,
|
||||
resolveRawData,
|
||||
} from './helpers/utils';
|
||||
import type { SetField, SetNodeOptions } from './helpers/interfaces';
|
||||
import { updateDisplayOptions } from '../../../utils/utilities';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Fields to Set',
|
||||
name: 'fields',
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
description: 'Edit existing fields or add new ones to modify the output data',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'values',
|
||||
displayName: 'Values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. fieldName',
|
||||
description:
|
||||
'Name of the field to set the value of. Supports dot-notation. Example: data.person[0].name.',
|
||||
requiresDataPath: 'single',
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
description: 'The field value type',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'String',
|
||||
value: 'stringValue',
|
||||
},
|
||||
{
|
||||
name: 'Number',
|
||||
value: 'numberValue',
|
||||
},
|
||||
{
|
||||
name: 'Boolean',
|
||||
value: 'booleanValue',
|
||||
},
|
||||
{
|
||||
name: 'Array',
|
||||
value: 'arrayValue',
|
||||
},
|
||||
{
|
||||
name: 'Object',
|
||||
value: 'objectValue',
|
||||
},
|
||||
],
|
||||
default: 'stringValue',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'stringValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['stringValue'],
|
||||
},
|
||||
},
|
||||
validateType: 'string',
|
||||
ignoreValidationDuringExecution: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'numberValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['numberValue'],
|
||||
},
|
||||
},
|
||||
validateType: 'number',
|
||||
ignoreValidationDuringExecution: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'booleanValue',
|
||||
type: 'options',
|
||||
default: 'true',
|
||||
options: [
|
||||
{
|
||||
name: 'True',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
name: 'False',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['booleanValue'],
|
||||
},
|
||||
},
|
||||
validateType: 'boolean',
|
||||
ignoreValidationDuringExecution: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'arrayValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. [ arrayItem1, arrayItem2, arrayItem3 ]',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['arrayValue'],
|
||||
},
|
||||
},
|
||||
validateType: 'array',
|
||||
ignoreValidationDuringExecution: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'objectValue',
|
||||
type: 'string',
|
||||
default: '={}',
|
||||
typeOptions: {
|
||||
editor: 'json',
|
||||
editorLanguage: 'json',
|
||||
rows: 2,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['objectValue'],
|
||||
},
|
||||
},
|
||||
validateType: 'object',
|
||||
ignoreValidationDuringExecution: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['manual'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
item: INodeExecutionData,
|
||||
i: number,
|
||||
options: SetNodeOptions,
|
||||
rawFieldsData: IDataObject,
|
||||
node: INode,
|
||||
) {
|
||||
try {
|
||||
const fields = this.getNodeParameter('fields.values', i, []) as SetField[];
|
||||
|
||||
const newData: IDataObject = {};
|
||||
|
||||
for (const entry of fields) {
|
||||
if (entry.type === 'objectValue' && rawFieldsData[entry.name] !== undefined) {
|
||||
entry.objectValue = parseJsonParameter(
|
||||
resolveRawData.call(this, rawFieldsData[entry.name] as string, i),
|
||||
node,
|
||||
i,
|
||||
entry.name,
|
||||
);
|
||||
}
|
||||
|
||||
const { name, value } = validateEntry(entry, node, i, options.ignoreConversionErrors);
|
||||
newData[name] = value;
|
||||
}
|
||||
|
||||
return composeReturnItem.call(this, i, item, newData, options);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return { json: { error: (error as Error).message } };
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), error as Error, {
|
||||
itemIndex: i,
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
69
packages/nodes-base/nodes/Set/v2/raw.mode.ts
Normal file
69
packages/nodes-base/nodes/Set/v2/raw.mode.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
INodeExecutionData,
|
||||
IExecuteFunctions,
|
||||
INodeProperties,
|
||||
IDataObject,
|
||||
INode,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { parseJsonParameter, composeReturnItem, resolveRawData } from './helpers/utils';
|
||||
import type { SetNodeOptions } from './helpers/interfaces';
|
||||
import { updateDisplayOptions } from '../../../utils/utilities';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'JSON Output',
|
||||
name: 'jsonOutput',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
editor: 'json',
|
||||
editorLanguage: 'json',
|
||||
rows: 5,
|
||||
},
|
||||
default: '{\n "my_field_1": "value",\n "my_field_2": 1\n}',
|
||||
validateType: 'object',
|
||||
ignoreValidationDuringExecution: true,
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['raw'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
item: INodeExecutionData,
|
||||
i: number,
|
||||
options: SetNodeOptions,
|
||||
rawData: IDataObject,
|
||||
node: INode,
|
||||
) {
|
||||
try {
|
||||
let newData: IDataObject;
|
||||
if (rawData.jsonOutput === undefined) {
|
||||
const json = this.getNodeParameter('jsonOutput', i) as string;
|
||||
newData = parseJsonParameter(json, node, i);
|
||||
} else {
|
||||
newData = parseJsonParameter(
|
||||
resolveRawData.call(this, rawData.jsonOutput as string, i),
|
||||
node,
|
||||
i,
|
||||
);
|
||||
}
|
||||
|
||||
return composeReturnItem.call(this, i, item, newData, options);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return { json: { error: (error as Error).message } };
|
||||
}
|
||||
throw new NodeOperationError(node, error as Error, {
|
||||
itemIndex: i,
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export class Snowflake implements INodeType {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
||||
@@ -68,6 +68,7 @@ export class TimescaleDb implements INodeType {
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
rows: 5,
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
|
||||
Reference in New Issue
Block a user