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:
Michael Kret
2023-09-19 13:16:35 +03:00
committed by GitHub
parent 050ba706d3
commit 3a474552b2
42 changed files with 2626 additions and 410 deletions

View File

@@ -7,6 +7,7 @@ const commonDescription: INodeProperties = {
typeOptions: {
editor: 'codeNodeEditor',
editorLanguage: 'javaScript',
rows: 5,
},
default: '',
description:

View File

@@ -7,6 +7,7 @@ const commonDescription: INodeProperties = {
typeOptions: {
editor: 'codeNodeEditor',
editorLanguage: 'python',
rows: 5,
},
default: '',
description:

View File

@@ -76,6 +76,7 @@ export class CrateDb implements INodeType {
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
rows: 5,
sqlDialect: 'PostgreSQL',
},
displayOptions: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -78,6 +78,7 @@ const versionDescription: INodeTypeDescription = {
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
rows: 5,
sqlDialect: 'MySQL',
},
displayOptions: {

View File

@@ -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',

View File

@@ -77,6 +77,7 @@ const versionDescription: INodeTypeDescription = {
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
rows: 5,
sqlDialect: 'PostgreSQL',
},
displayOptions: {

View File

@@ -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',

View File

@@ -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[] = [];

View File

@@ -63,6 +63,7 @@ export class QuestDb implements INodeType {
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
rows: 5,
sqlDialect: 'PostgreSQL',
},
displayOptions: {

View File

@@ -121,7 +121,7 @@
}
]
},
"alias": ["JSON", "Filter", "Transform", "Map"],
"alias": ["JSON", "Filter", "Transform", "Map", "Set"],
"subcategories": {
"Core Nodes": ["Data Transformation"]
}

View File

@@ -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);
}
}

View 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": []
}

View 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',
});
});
});

View 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];
}
}

View 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];
}
}

View 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];

View 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;
}

View 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,
});
}
}

View 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,
});
}
}

View File

@@ -69,6 +69,7 @@ export class Snowflake implements INodeType {
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
rows: 5,
},
displayOptions: {
show: {

View File

@@ -68,6 +68,7 @@ export class TimescaleDb implements INodeType {
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
rows: 5,
sqlDialect: 'PostgreSQL',
},
displayOptions: {