import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type {
PgpDatabase,
QueriesRunner,
QueryValues,
QueryWithValues,
} from '../../helpers/interfaces';
import {
addReturning,
checkItemAgainstSchema,
doesRowExist,
getTableSchema,
prepareItem,
replaceEmptyStringsByNulls,
} from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Data Mode',
name: 'dataMode',
type: 'options',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMapInputData',
description: 'Use when node input properties names exactly match the table column names',
},
{
name: 'Map Each Column Manually',
value: 'defineBelow',
description: 'Set the value for each destination column manually',
},
],
default: 'autoMapInputData',
description:
'Whether to map node input properties and the table data automatically or manually',
displayOptions: {
show: {
'@version': [2, 2.1],
},
},
},
{
displayName: `
In this mode, make sure incoming data fields are named the same as the columns in your table. If needed, use a 'Set' node before this node to change the field names.
`,
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
dataMode: ['autoMapInputData'],
'@version': [2],
},
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column to Match On',
name: 'columnToMatchOn',
type: 'options',
required: true,
description:
'The column to compare when finding the rows to update. Choose from the list, or specify an ID using an expression.',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: ['schema.value', 'table.value'],
},
default: '',
hint: 'The column that identifies the row(s) to modify',
displayOptions: {
show: {
'@version': [2, 2.1],
},
},
},
{
displayName: 'Value of Column to Match On',
name: 'valueToMatchOn',
type: 'string',
default: '',
description:
'Rows with a value in the specified "Column to Match On" that corresponds to the value in this field will be updated',
displayOptions: {
show: {
dataMode: ['defineBelow'],
'@version': [2, 2.1],
},
},
},
{
displayName: 'Values to Send',
name: 'valuesToSend',
placeholder: 'Add Value',
type: 'fixedCollection',
typeOptions: {
multipleValueButtonText: 'Add Value',
multipleValues: true,
},
displayOptions: {
show: {
dataMode: ['defineBelow'],
'@version': [2, 2.1],
},
},
default: {},
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column',
name: 'column',
type: 'options',
description:
'Choose from the list, or specify an ID using an expression',
typeOptions: {
loadOptionsMethod: 'getColumnsWithoutColumnToMatchOn',
loadOptionsDependsOn: ['schema.value', 'table.value'],
},
default: [],
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Columns',
name: 'columns',
type: 'resourceMapper',
noDataExpression: true,
default: {
mappingMode: 'defineBelow',
value: null,
},
required: true,
typeOptions: {
loadOptionsDependsOn: ['table.value', 'operation'],
resourceMapper: {
resourceMapperMethod: 'getMappingColumns',
mode: 'update',
fieldWords: {
singular: 'column',
plural: 'columns',
},
addAllFields: true,
multiKeyMatch: true,
},
},
displayOptions: {
show: {
'@version': [2.2],
},
},
},
optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['update'],
},
hide: {
table: [''],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: IDataObject,
db: PgpDatabase,
): Promise {
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
const queries: QueryWithValues[] = [];
for (let i = 0; i < items.length; i++) {
const schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
const table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
const nodeVersion = this.getNode().typeVersion;
const columnsToMatchOn: string[] =
nodeVersion < 2.2
? [this.getNodeParameter('columnToMatchOn', i) as string]
: (this.getNodeParameter('columns.matchingColumns', i) as string[]);
const dataMode =
nodeVersion < 2.2
? (this.getNodeParameter('dataMode', i) as string)
: (this.getNodeParameter('columns.mappingMode', i) as string);
let item: IDataObject = {};
let valueToMatchOn: string | IDataObject = '';
if (nodeVersion < 2.2) {
valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
}
if (dataMode === 'autoMapInputData') {
item = items[i].json;
if (nodeVersion < 2.2) {
valueToMatchOn = item[columnsToMatchOn[0]] as string;
}
}
if (dataMode === 'defineBelow') {
const valuesToSend =
nodeVersion < 2.2
? ((this.getNodeParameter('valuesToSend', i, []) as IDataObject).values as IDataObject[])
: ((this.getNodeParameter('columns.values', i, []) as IDataObject)
.values as IDataObject[]);
if (nodeVersion < 2.2) {
item = prepareItem(valuesToSend);
item[columnsToMatchOn[0]] = this.getNodeParameter('valueToMatchOn', i) as string;
} else {
item = this.getNodeParameter('columns.value', i) as IDataObject;
}
}
const matchValues: string[] = [];
if (nodeVersion < 2.2) {
if (!item[columnsToMatchOn[0]] && dataMode === 'autoMapInputData') {
throw new NodeOperationError(
this.getNode(),
"Column to match on not found in input item. Add a column to match on or set the 'Data Mode' to 'Define Below' to define the value to match on.",
);
}
matchValues.push(valueToMatchOn);
matchValues.push(columnsToMatchOn[0]);
} else {
columnsToMatchOn.forEach((column) => {
matchValues.push(column);
matchValues.push(item[column] as string);
});
const rowExists = await doesRowExist(db, schema, table, matchValues);
if (!rowExists) {
const descriptionValues: string[] = [];
matchValues.forEach((val, index) => {
if (index % 2 === 0) {
descriptionValues.push(`${matchValues[index]}=${matchValues[index + 1]}`);
}
});
throw new NodeOperationError(
this.getNode(),
"The row you are trying to update doesn't exist",
{
description: `No rows matching the provided values (${descriptionValues.join(
', ',
)}) were found in the table "${table}".`,
itemIndex: i,
},
);
}
}
const tableSchema = await getTableSchema(db, schema, table);
item = checkItemAgainstSchema(this.getNode(), item, tableSchema, i);
let values: QueryValues = [schema, table];
let valuesLength = values.length + 1;
let condition = '';
if (nodeVersion < 2.2) {
condition = `$${valuesLength}:name = $${valuesLength + 1}`;
valuesLength = valuesLength + 2;
values.push(columnsToMatchOn[0], valueToMatchOn);
} else {
const conditions: string[] = [];
for (const column of columnsToMatchOn) {
conditions.push(`$${valuesLength}:name = $${valuesLength + 1}`);
valuesLength = valuesLength + 2;
values.push(column, item[column] as string);
}
condition = conditions.join(' AND ');
}
const updateColumns = Object.keys(item).filter((column) => !columnsToMatchOn.includes(column));
if (!Object.keys(updateColumns).length) {
throw new NodeOperationError(
this.getNode(),
"Add values to update to the input item or set the 'Data Mode' to 'Define Below' to define the values to update.",
);
}
const updates: string[] = [];
for (const column of updateColumns) {
updates.push(`$${valuesLength}:name = $${valuesLength + 1}`);
valuesLength = valuesLength + 2;
values.push(column, item[column] as string);
}
let query = `UPDATE $1:name.$2:name SET ${updates.join(', ')} WHERE ${condition}`;
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];
[query, values] = addReturning(query, outputColumns, values);
queries.push({ query, values });
}
const results = await runQueries(queries, items, nodeOptions);
return results;
}