2023-04-03 08:18:01 -07:00
import type { IExecuteFunctions } from 'n8n-core' ;
import type { IDataObject , INodeExecutionData , INodeProperties } from 'n8n-workflow' ;
2023-05-19 06:42:24 -07:00
import { NodeOperationError } from 'n8n-workflow' ;
2023-04-03 08:18:01 -07:00
import { updateDisplayOptions } from '../../../../../utils/utilities' ;
import type {
PgpDatabase ,
QueriesRunner ,
QueryValues ,
QueryWithValues ,
} from '../../helpers/interfaces' ;
import {
addReturning ,
checkItemAgainstSchema ,
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' ,
} ,
{
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' ] ,
} ,
} ,
} ,
{
// 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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.' ,
typeOptions : {
loadOptionsMethod : 'getColumns' ,
loadOptionsDependsOn : [ 'schema.value' , 'table.value' ] ,
} ,
default : '' ,
hint : "Used to find the correct row(s) to update. Doesn't get changed. Has to be unique." ,
} ,
{
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. New rows will be created for non-matching items.' ,
displayOptions : {
show : {
dataMode : [ 'defineBelow' ] ,
} ,
} ,
} ,
{
displayName : 'Values to Send' ,
name : 'valuesToSend' ,
placeholder : 'Add Value' ,
type : 'fixedCollection' ,
typeOptions : {
multipleValueButtonText : 'Add Value' ,
multipleValues : true ,
} ,
displayOptions : {
show : {
dataMode : [ 'defineBelow' ] ,
} ,
} ,
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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>' ,
typeOptions : {
loadOptionsMethod : 'getColumnsWithoutColumnToMatchOn' ,
loadOptionsDependsOn : [ 'schema.value' , 'table.value' ] ,
} ,
default : [ ] ,
} ,
{
displayName : 'Value' ,
name : 'value' ,
type : 'string' ,
default : '' ,
} ,
] ,
} ,
] ,
} ,
optionsCollection ,
] ;
const displayOptions = {
show : {
resource : [ 'database' ] ,
operation : [ 'upsert' ] ,
} ,
hide : {
table : [ '' ] ,
} ,
} ;
export const description = updateDisplayOptions ( displayOptions , properties ) ;
export async function execute (
this : IExecuteFunctions ,
runQueries : QueriesRunner ,
items : INodeExecutionData [ ] ,
nodeOptions : IDataObject ,
db : PgpDatabase ,
) : Promise < INodeExecutionData [ ] > {
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 columnToMatchOn = this . getNodeParameter ( 'columnToMatchOn' , i ) as string ;
const dataMode = this . getNodeParameter ( 'dataMode' , i ) as string ;
let item : IDataObject = { } ;
if ( dataMode === 'autoMapInputData' ) {
item = items [ i ] . json ;
}
if ( dataMode === 'defineBelow' ) {
const valuesToSend = ( this . getNodeParameter ( 'valuesToSend' , i , [ ] ) as IDataObject )
. values as IDataObject [ ] ;
item = prepareItem ( valuesToSend ) ;
item [ columnToMatchOn ] = this . getNodeParameter ( 'valueToMatchOn' , i ) as string ;
}
2023-05-19 06:42:24 -07:00
if ( ! item [ columnToMatchOn ] ) {
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." ,
) ;
}
if ( item [ columnToMatchOn ] && Object . keys ( item ) . length === 1 ) {
throw new NodeOperationError (
this . getNode ( ) ,
"Add values to update or insert to the input item or set the 'Data Mode' to 'Define Below' to define the values to insert or update." ,
) ;
}
2023-04-03 08:18:01 -07:00
const tableSchema = await getTableSchema ( db , schema , table ) ;
item = checkItemAgainstSchema ( this . getNode ( ) , item , tableSchema , i ) ;
let values : QueryValues = [ schema , table ] ;
let valuesLength = values . length + 1 ;
const onConflict = ` ON CONFLICT ( $ ${ valuesLength } :name) DO UPDATE ` ;
valuesLength = valuesLength + 1 ;
values . push ( columnToMatchOn ) ;
const insertQuery = ` INSERT INTO $ 1:name. $ 2:name( $ ${ valuesLength } :name) VALUES( $ ${ valuesLength } :csv) ${ onConflict } ` ;
valuesLength = valuesLength + 1 ;
values . push ( item ) ;
const updateColumns = Object . keys ( item ) . filter ( ( column ) = > column !== columnToMatchOn ) ;
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 = ` ${ insertQuery } SET ${ updates . join ( ', ' ) } ` ;
const outputColumns = this . getNodeParameter ( 'options.outputColumns' , i , [ '*' ] ) as string [ ] ;
[ query , values ] = addReturning ( query , outputColumns , values ) ;
queries . push ( { query , values } ) ;
}
return runQueries ( queries , items , nodeOptions ) ;
}