first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,370 @@
import type { INodeProperties, INodePropertyOptions } from 'n8n-workflow';
export const operatorOptions: INodePropertyOptions[] = [
{
name: 'Equal',
value: 'equal',
},
{
name: 'Not Equal',
value: '!=',
},
{
name: 'Like',
value: 'LIKE',
},
{
name: 'Greater Than',
value: '>',
},
{
name: 'Less Than',
value: '<',
},
{
name: 'Greater Than Or Equal',
value: '>=',
},
{
name: 'Less Than Or Equal',
value: '<=',
},
{
name: 'Is Null',
value: 'IS NULL',
},
{
name: 'Is Not Null',
value: 'IS NOT NULL',
},
];
export const optionsCollection: INodeProperties = {
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Cascade',
name: 'cascade',
type: 'boolean',
default: false,
description:
'Whether to drop all objects that depend on the table, such as views and sequences',
displayOptions: {
show: {
'/operation': ['deleteTable'],
},
hide: {
'/deleteCommand': ['delete'],
},
},
},
{
displayName: 'Connection Timeout',
name: 'connectionTimeout',
type: 'number',
default: 30,
description: 'Number of seconds reserved for connecting to the database',
},
{
displayName: 'Delay Closing Idle Connection',
name: 'delayClosingIdleConnection',
type: 'number',
default: 0,
description: 'Number of seconds to wait before idle connection would be eligible for closing',
typeOptions: {
minValue: 0,
},
},
{
displayName: 'Query Batching',
name: 'queryBatching',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Single Query',
value: 'single',
description: 'A single query for all incoming items',
},
{
name: 'Independent',
value: 'independently',
description: 'Execute one query per incoming item of the run',
},
{
name: 'Transaction',
value: 'transaction',
description:
'Execute all queries in a transaction, if a failure occurs, all changes are rolled back',
},
],
default: 'single',
description: 'The way queries should be sent to the database',
},
{
displayName: 'Query Parameters',
name: 'queryReplacement',
type: 'string',
default: '',
description:
'Comma-separated list of the values you want to use as query parameters. <a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres/#use-query-parameters" target="_blank">More info</a>.',
hint: 'Comma-separated list of values: reference them in your query as $1, $2, $3…',
placeholder: 'e.g. value1,value2,value3',
displayOptions: {
show: { '/operation': ['executeQuery'] },
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName: 'Treat query parameters in single quotes as text',
name: 'treatQueryParametersInSingleQuotesAsText',
type: 'boolean',
default: false,
description: "Whether to treat query parameters enclosed in single quotes as text e.g. '$1'",
displayOptions: {
show: { queryReplacement: [{ _cnd: { exists: true } }] },
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Output Columns',
name: 'outputColumns',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/" target="_blank">expression</a>',
typeOptions: {
loadOptionsMethod: 'getColumnsMultiOptions',
loadOptionsDependsOn: ['table.value'],
},
default: [],
displayOptions: {
show: { '/operation': ['select', 'insert', 'update', 'upsert'] },
},
},
{
displayName: 'Output Large-Format Numbers As',
name: 'largeNumbersOutput',
type: 'options',
options: [
{
name: 'Numbers',
value: 'numbers',
},
{
name: 'Text',
value: 'text',
description:
'Use this if you expect numbers longer than 16 digits (otherwise numbers may be incorrect)',
},
],
hint: 'Applies to NUMERIC and BIGINT columns only',
default: 'text',
},
{
displayName: 'Skip on Conflict',
name: 'skipOnConflict',
type: 'boolean',
default: false,
description:
'Whether to skip the row and do not throw error if a unique constraint or exclusion constraint is violated',
displayOptions: {
show: {
'/operation': ['insert'],
},
},
},
{
displayName: 'Replace Empty Strings with NULL',
name: 'replaceEmptyStrings',
type: 'boolean',
default: false,
description:
'Whether to replace empty strings with NULL in input, could be useful when data come from spreadsheet',
displayOptions: {
show: {
'/operation': ['insert', 'update', 'upsert', 'executeQuery'],
},
},
},
],
};
export const schemaRLC: INodeProperties = {
displayName: 'Schema',
name: 'schema',
type: 'resourceLocator',
default: { mode: 'list', value: 'public' },
required: true,
placeholder: 'e.g. public',
description: 'The schema that contains the table you want to work on',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'schemaSearch',
},
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
},
],
};
export const tableRLC: INodeProperties = {
displayName: 'Table',
name: 'table',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
description: 'The table you want to work on',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'tableSearch',
},
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
},
],
};
export const whereFixedCollection: INodeProperties = {
displayName: 'Select Rows',
name: 'where',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
placeholder: 'Add Condition',
default: {},
description: 'If not set, all rows will be selected',
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',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/" target="_blank">expression</a>',
default: '',
placeholder: 'e.g. ID',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: ['schema.value', 'table.value'],
},
},
{
displayName: 'Operator',
name: 'condition',
type: 'options',
description:
"The operator to check the column against. When using 'LIKE' operator percent sign ( %) matches zero or more characters, underscore ( _ ) matches any single character.",
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: operatorOptions,
default: 'equal',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
displayOptions: {
hide: {
condition: ['IS NULL', 'IS NOT NULL'],
},
},
default: '',
},
],
},
],
};
export const sortFixedCollection: INodeProperties = {
displayName: 'Sort',
name: 'sort',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
placeholder: 'Add Sort Rule',
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',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/" target="_blank">expression</a>',
default: '',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: ['schema.value', 'table.value'],
},
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'ASC',
value: 'ASC',
},
{
name: 'DESC',
value: 'DESC',
},
],
default: 'ASC',
},
],
},
],
};
export const combineConditionsCollection: INodeProperties = {
displayName: 'Combine Conditions',
name: 'combineConditions',
type: 'options',
description:
'How to combine the conditions defined in "Select Rows": AND requires all conditions to be true, OR requires at least one condition to be true',
options: [
{
name: 'AND',
value: 'AND',
description: 'Only rows that meet all the conditions are selected',
},
{
name: 'OR',
value: 'OR',
description: 'Rows that meet at least one condition are selected',
},
],
default: 'AND',
};
@@ -0,0 +1,74 @@
import type { INodeProperties } from 'n8n-workflow';
import * as deleteTable from './deleteTable.operation';
import * as executeQuery from './executeQuery.operation';
import * as insert from './insert.operation';
import * as select from './select.operation';
import * as update from './update.operation';
import * as upsert from './upsert.operation';
import { schemaRLC, tableRLC } from '../common.descriptions';
export { deleteTable, executeQuery, insert, select, update, upsert };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Delete',
value: 'deleteTable',
description: 'Delete an entire table or rows in a table',
action: 'Delete table or rows',
},
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Execute an SQL query',
action: 'Execute a SQL query',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in a table',
action: 'Insert rows in a table',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-option-name-wrong-for-upsert
name: 'Insert or Update',
value: 'upsert',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-upsert
description: 'Insert or update rows in a table',
action: 'Insert or update rows in a table',
},
{
name: 'Select',
value: 'select',
description: 'Select rows from a table',
action: 'Select rows from a table',
},
{
name: 'Update',
value: 'update',
description: 'Update rows in a table',
action: 'Update rows in a table',
},
],
displayOptions: {
show: {
resource: ['database'],
},
},
default: 'insert',
},
{ ...schemaRLC, displayOptions: { hide: { operation: ['executeQuery'] } } },
{ ...tableRLC, displayOptions: { hide: { operation: ['executeQuery'] } } },
...deleteTable.description,
...executeQuery.description,
...insert.description,
...select.description,
...update.description,
...upsert.description,
];
@@ -0,0 +1,155 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type {
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
QueryValues,
QueryWithValues,
} from '../../helpers/interfaces';
import { addWhereClauses, getWhereClauses } from '../../helpers/utils';
import {
combineConditionsCollection,
optionsCollection,
whereFixedCollection,
} from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Command',
name: 'deleteCommand',
type: 'options',
default: 'truncate',
options: [
{
name: 'Truncate',
value: 'truncate',
description: "Only removes the table's data and preserves the table's structure",
},
{
name: 'Delete',
value: 'delete',
description:
"Delete the rows that match the 'Select Rows' conditions below. If no selection is made, all rows in the table are deleted.",
},
{
name: 'Drop',
value: 'drop',
description: "Deletes the table's data and also the table's structure permanently",
},
],
},
{
displayName: 'Restart Sequences',
name: 'restartSequences',
type: 'boolean',
default: false,
description: 'Whether to reset identity (auto-increment) columns to their initial values',
displayOptions: {
show: {
deleteCommand: ['truncate'],
},
},
},
{
...whereFixedCollection,
displayOptions: {
show: {
deleteCommand: ['delete'],
},
},
},
{
...combineConditionsCollection,
displayOptions: {
show: {
deleteCommand: ['delete'],
},
},
},
optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['deleteTable'],
},
hide: {
table: [''],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: PostgresNodeOptions,
_db?: PgpDatabase,
): Promise<INodeExecutionData[]> {
const queries: QueryWithValues[] = [];
for (let i = 0; i < items.length; i++) {
const options = this.getNodeParameter('options', i, {});
const schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
const table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
const deleteCommand = this.getNodeParameter('deleteCommand', i) as string;
let query = '';
let values: QueryValues = [schema, table];
if (deleteCommand === 'drop') {
const cascade = options.cascade ? ' CASCADE' : '';
query = `DROP TABLE IF EXISTS $1:name.$2:name${cascade}`;
}
if (deleteCommand === 'truncate') {
const identity = this.getNodeParameter('restartSequences', i, false)
? ' RESTART IDENTITY'
: '';
const cascade = options.cascade ? ' CASCADE' : '';
query = `TRUNCATE TABLE $1:name.$2:name${identity}${cascade}`;
}
if (deleteCommand === 'delete') {
const whereClauses = getWhereClauses(this, i);
const combineConditions = this.getNodeParameter('combineConditions', i, 'AND') as string;
[query, values] = addWhereClauses(
this.getNode(),
i,
'DELETE FROM $1:name.$2:name',
whereClauses,
values,
combineConditions,
);
}
if (query === '') {
throw new NodeOperationError(
this.getNode(),
'Invalid delete command, only drop, delete and truncate are supported ',
{ itemIndex: i },
);
}
const queryWithValues = { query, values };
queries.push(queryWithValues);
}
return await runQueries(queries, nodeOptions);
}
@@ -0,0 +1,150 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
import type {
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
QueryWithValues,
} from '../../helpers/interfaces';
import {
evaluateExpression,
isJSON,
replaceEmptyStringsByNulls,
stringToArray,
} from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
placeholder: 'e.g. SELECT id, name FROM product WHERE quantity > $1 AND price <= $2',
noDataExpression: true,
required: true,
description:
"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',
sqlDialect: 'PostgreSQL',
},
hint: 'Consider using query parameters to prevent SQL injection attacks. Add them in the options below',
},
optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['executeQuery'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: PostgresNodeOptions,
_db?: PgpDatabase,
): Promise<INodeExecutionData[]> {
const queries: QueryWithValues[] = replaceEmptyStringsByNulls(
items,
nodeOptions.replaceEmptyStrings as boolean,
).map((_, index) => {
let query = this.getNodeParameter('query', index) as string;
for (const resolvable of getResolvables(query)) {
query = query.replace(resolvable, this.evaluateExpression(resolvable, index) as string);
}
let values: Array<IDataObject | string> = [];
let queryReplacement = this.getNodeParameter('options.queryReplacement', index, '');
if (typeof queryReplacement === 'number') {
queryReplacement = String(queryReplacement);
}
if (typeof queryReplacement === 'string') {
const node = this.getNode();
const rawReplacements = (node.parameters.options as IDataObject)?.queryReplacement as string;
if (rawReplacements) {
const nodeVersion = nodeOptions.nodeVersion as number;
if (nodeVersion >= 2.5) {
const rawValues = rawReplacements.replace(/^=+/, '');
const resolvables = getResolvables(rawValues);
if (resolvables.length) {
for (const resolvable of resolvables) {
const evaluatedExpression = evaluateExpression(
this.evaluateExpression(`${resolvable}`, index),
);
const evaluatedValues = isJSON(evaluatedExpression)
? [evaluatedExpression]
: stringToArray(evaluatedExpression);
if (evaluatedValues.length) values.push(...evaluatedValues);
}
} else {
values.push(...stringToArray(rawValues));
}
} else {
const rawValues = rawReplacements
.replace(/^=+/, '')
.split(',')
.filter((entry) => entry)
.map((entry) => entry.trim());
for (const rawValue of rawValues) {
const resolvables = getResolvables(rawValue);
if (resolvables.length) {
for (const resolvable of resolvables) {
values.push(this.evaluateExpression(`${resolvable}`, index) as IDataObject);
}
} else {
values.push(rawValue);
}
}
}
}
} else {
if (Array.isArray(queryReplacement)) {
values = queryReplacement as IDataObject[];
} else {
throw new NodeOperationError(
this.getNode(),
'Query Parameters must be a string of comma-separated values or an array of values',
{ itemIndex: index },
);
}
}
if (!queryReplacement || nodeOptions.treatQueryParametersInSingleQuotesAsText) {
let nextValueIndex = values.length + 1;
const literals = query.match(/'\$[0-9]+'/g) ?? [];
for (const literal of literals) {
query = query.replace(literal, `$${nextValueIndex}`);
values.push(literal.replace(/'/g, ''));
nextValueIndex++;
}
}
return { query, values, options: { partial: true } };
});
return await runQueries(queries, nodeOptions);
}
@@ -0,0 +1,266 @@
import {
type IDataObject,
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type {
PgpClient,
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
QueryValues,
QueryWithValues,
} from '../../helpers/interfaces';
import {
addReturning,
checkItemAgainstSchema,
configureTableSchemaUpdater,
getTableSchema,
prepareItem,
convertArraysToPostgresFormat,
replaceEmptyStringsByNulls,
hasJsonDataTypeInSchema,
convertValuesToJsonWithPgp,
runQueriesAndHandleErrors,
} 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 an 'Edit Fields' node before this node to change the field names.
`,
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
dataMode: ['autoMapInputData'],
'@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',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/" target="_blank">expression</a>',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: ['schema.value', 'table.value'],
},
default: [],
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Columns',
name: 'columns',
type: 'resourceMapper',
default: {
mappingMode: 'defineBelow',
value: null,
},
noDataExpression: true,
required: true,
typeOptions: {
loadOptionsDependsOn: ['table.value', 'operation'],
resourceMapper: {
resourceMapperMethod: 'getMappingColumns',
mode: 'add',
fieldWords: {
singular: 'column',
plural: 'columns',
},
addAllFields: true,
multiKeyMatch: true,
},
},
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 2.2 } }],
},
},
},
optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['insert'],
},
hide: {
table: [''],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: PostgresNodeOptions,
db: PgpDatabase,
pgp: PgpClient,
): Promise<INodeExecutionData[]> {
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
const nodeVersion = nodeOptions.nodeVersion as number;
let schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
let table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
const updateTableSchema = configureTableSchemaUpdater(schema, table);
let tableSchema = await getTableSchema(db, schema, table);
const queries: QueryWithValues[] = [];
const errorItemsMap = new Map<number, INodeExecutionData>();
for (let i = 0; i < items.length; i++) {
try {
schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
const options = this.getNodeParameter('options', i, {});
let onConflict = '';
if (options.skipOnConflict) {
onConflict = ' ON CONFLICT DO NOTHING';
}
let query = `INSERT INTO $1:name.$2:name($3:name) VALUES($3:csv)${onConflict}`;
let values: QueryValues = [schema, table];
const dataMode =
nodeVersion < 2.2
? (this.getNodeParameter('dataMode', i) as string)
: (this.getNodeParameter('columns.mappingMode', i) as string);
let item: IDataObject = {};
if (dataMode === 'autoMapInputData') {
item = items[i].json;
}
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[]);
item =
nodeVersion < 2.2
? prepareItem(valuesToSend)
: hasJsonDataTypeInSchema(tableSchema)
? convertValuesToJsonWithPgp(
pgp,
tableSchema,
(this.getNodeParameter('columns', i) as IDataObject)?.value as IDataObject,
)
: (this.getNodeParameter('columns.value', i) as IDataObject);
}
tableSchema = await updateTableSchema(db, tableSchema, schema, table);
if (nodeVersion >= 2.4) {
item = convertArraysToPostgresFormat(item, tableSchema, this.getNode(), i);
}
values.push(checkItemAgainstSchema(this.getNode(), item, tableSchema, i));
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];
if (nodeVersion >= 2.6 && Object.keys(item).length === 0) {
query = 'INSERT INTO $1:name.$2:name DEFAULT VALUES';
}
[query, values] = addReturning(query, outputColumns, values);
queries.push({ query, values });
} catch (e) {
if (this.continueOnFail()) {
const error = e instanceof Error ? e : String(e);
errorItemsMap.set(i, { json: { error }, pairedItem: { item: i } });
continue;
}
throw e;
}
}
return await runQueriesAndHandleErrors(runQueries, queries, nodeOptions, errorItemsMap);
}
@@ -0,0 +1,143 @@
import {
tryToParseNumber,
type IDataObject,
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type {
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
QueryValues,
QueryWithValues,
SortRule,
} from '../../helpers/interfaces';
import {
addSortRules,
addWhereClauses,
getWhereClauses,
replaceEmptyStringsByNulls,
} from '../../helpers/utils';
import {
combineConditionsCollection,
optionsCollection,
sortFixedCollection,
whereFixedCollection,
} from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['event'],
operation: ['getAll'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
returnAll: [false],
},
},
},
whereFixedCollection,
combineConditionsCollection,
sortFixedCollection,
optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['select'],
},
hide: {
table: [''],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: PostgresNodeOptions,
_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;
let values: QueryValues = [schema, table];
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];
let query = '';
if (outputColumns.includes('*')) {
query = 'SELECT * FROM $1:name.$2:name';
} else {
values.push(outputColumns);
query = `SELECT $${values.length}:name FROM $1:name.$2:name`;
}
const whereClauses = getWhereClauses(this, i);
const combineConditions = this.getNodeParameter('combineConditions', i, 'AND') as string;
[query, values] = addWhereClauses(
this.getNode(),
i,
query,
whereClauses,
values,
combineConditions,
);
const sortRules =
((this.getNodeParameter('sort', i, []) as IDataObject).values as SortRule[]) || [];
[query, values] = addSortRules(query, sortRules, values);
const returnAll = this.getNodeParameter('returnAll', i, false);
if (!returnAll) {
const limitRaw = this.getNodeParameter('limit', i, 50);
const limit = tryToParseNumber(limitRaw);
values.push(limit);
query += ` LIMIT $${values.length}`;
}
const queryWithValues = { query, values };
queries.push(queryWithValues);
}
return await runQueries(queries, nodeOptions);
}
@@ -0,0 +1,370 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type {
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
QueryValues,
QueryWithValues,
} from '../../helpers/interfaces';
import {
addReturning,
checkItemAgainstSchema,
configureTableSchemaUpdater,
doesRowExist,
getTableSchema,
prepareItem,
convertArraysToPostgresFormat,
replaceEmptyStringsByNulls,
runQueriesAndHandleErrors,
} 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 an 'Edit Fields' 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-wrong-for-dynamic-options
displayName: 'Column to Match On',
name: 'columnToMatchOn',
type: 'options',
required: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
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/expressions/" target="_blank">expression</a>.',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: ['schema.value', 'table.value'],
},
default: '',
hint: 'The column to use when matching rows in Postgres to the input items of this node. Usually an ID.',
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',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/" target="_blank">expression</a>',
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': [{ _cnd: { gte: 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: PostgresNodeOptions,
db: PgpDatabase,
): Promise<INodeExecutionData[]> {
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
const nodeVersion = nodeOptions.nodeVersion as number;
let schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
let table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
const updateTableSchema = configureTableSchemaUpdater(schema, table);
let tableSchema = await getTableSchema(db, schema, table);
const queries: QueryWithValues[] = [];
const errorItemsMap = new Map<number, INodeExecutionData>();
for (let i = 0; i < items.length; i++) {
try {
schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
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((_, 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,
},
);
}
}
tableSchema = await updateTableSchema(db, tableSchema, schema, table);
if (nodeVersion >= 2.4) {
item = convertArraysToPostgresFormat(item, tableSchema, this.getNode(), i);
}
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 });
} catch (e) {
if (this.continueOnFail()) {
const error = e instanceof Error ? e : String(e);
errorItemsMap.set(i, { json: { error }, pairedItem: { item: i } });
continue;
}
throw e;
}
}
return await runQueriesAndHandleErrors(runQueries, queries, nodeOptions, errorItemsMap);
}
@@ -0,0 +1,329 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type {
PgpDatabase,
PostgresNodeOptions,
QueriesRunner,
QueryValues,
QueryWithValues,
} from '../../helpers/interfaces';
import {
addReturning,
checkItemAgainstSchema,
getTableSchema,
prepareItem,
replaceEmptyStringsByNulls,
configureTableSchemaUpdater,
convertArraysToPostgresFormat,
runQueriesAndHandleErrors,
} 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 an 'Edit Fields' node before this node to change the field names.
`,
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
dataMode: ['autoMapInputData'],
'@version': [2, 2.1],
},
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Unique Column',
name: 'columnToMatchOn',
type: 'options',
required: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
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/expressions/" target="_blank">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.",
displayOptions: {
show: {
'@version': [2, 2.1],
},
},
},
{
displayName: 'Value of Unique Column',
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'],
'@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',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/" target="_blank">expression</a>',
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: 'upsert',
fieldWords: {
singular: 'column',
plural: 'columns',
},
addAllFields: true,
multiKeyMatch: true,
},
},
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 2.2 } }],
},
},
},
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: PostgresNodeOptions,
db: PgpDatabase,
): Promise<INodeExecutionData[]> {
items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean);
const nodeVersion = nodeOptions.nodeVersion as number;
let schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
let table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
const updateTableSchema = configureTableSchemaUpdater(schema, table);
let tableSchema = await getTableSchema(db, schema, table);
const queries: QueryWithValues[] = [];
const errorItemsMap = new Map<number, INodeExecutionData>();
for (let i = 0; i < items.length; i++) {
try {
schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
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 = {};
if (dataMode === 'autoMapInputData') {
item = items[i].json;
}
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;
}
}
if (!item[columnsToMatchOn[0]]) {
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[columnsToMatchOn[0]] && 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.",
);
}
tableSchema = await updateTableSchema(db, tableSchema, schema, table);
if (nodeVersion >= 2.4) {
item = convertArraysToPostgresFormat(item, tableSchema, this.getNode(), i);
}
item = checkItemAgainstSchema(this.getNode(), item, tableSchema, i);
let values: QueryValues = [schema, table];
let valuesLength = values.length + 1;
const conflictColumns: string[] = [];
columnsToMatchOn.forEach((column) => {
conflictColumns.push(`$${valuesLength}:name`);
valuesLength = valuesLength + 1;
values.push(column);
});
const onConflict = ` ON CONFLICT (${conflictColumns.join(',')})`;
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) => !columnsToMatchOn.includes(column),
);
const updates: string[] = [];
for (const column of updateColumns) {
updates.push(`$${valuesLength}:name = $${valuesLength + 1}`);
valuesLength = valuesLength + 2;
values.push(column, item[column] as string);
}
const updateQuery =
updates?.length > 0 ? ` DO UPDATE SET ${updates.join(', ')}` : ' DO NOTHING ';
let query = `${insertQuery}${updateQuery}`;
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];
[query, values] = addReturning(query, outputColumns, values);
queries.push({ query, values });
} catch (e) {
if (this.continueOnFail()) {
const error = e instanceof Error ? e : String(e);
errorItemsMap.set(i, { json: { error }, pairedItem: { item: i } });
continue;
}
throw e;
}
}
return await runQueriesAndHandleErrors(runQueries, queries, nodeOptions, errorItemsMap);
}
@@ -0,0 +1,9 @@
import type { AllEntities, Entity } from 'n8n-workflow';
type PostgresMap = {
database: 'deleteTable' | 'executeQuery' | 'insert' | 'select' | 'update' | 'upsert';
};
export type PostgresType = AllEntities<PostgresMap>;
export type PostgresDatabaseType = Entity<PostgresMap, 'database'>;
@@ -0,0 +1,60 @@
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import * as database from './database/Database.resource';
import type { PostgresType } from './node.type';
import { addExecutionHints } from '../../../../utils/utilities';
import { configurePostgres } from '../../transport';
import type { PostgresNodeCredentials, PostgresNodeOptions } from '../helpers/interfaces';
import { configureQueryRunner } from '../helpers/utils';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
let returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const resource = this.getNodeParameter<PostgresType>('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const credentials = await this.getCredentials<PostgresNodeCredentials>('postgres');
const options = this.getNodeParameter('options', 0, {}) as PostgresNodeOptions;
const node = this.getNode();
options.nodeVersion = node.typeVersion;
options.operation = operation;
const { db, pgp } = await configurePostgres.call(this, credentials, options);
const runQueries = configureQueryRunner.call(
this,
this.getNode(),
this.continueOnFail(),
pgp,
db,
);
const postgresNodeData = {
resource,
operation,
} as PostgresType;
switch (postgresNodeData.resource) {
case 'database':
returnData = await database[postgresNodeData.operation].execute.call(
this,
runQueries,
items,
options,
db,
pgp,
);
break;
default:
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported!`,
);
}
addExecutionHints(this, node, items, operation, node.executeOnce);
return [returnData];
}
@@ -0,0 +1,43 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import * as database from './database/Database.resource';
export const versionDescription: INodeTypeDescription = {
displayName: 'Postgres',
name: 'postgres',
icon: 'file:postgres.svg',
group: ['input'],
version: [2, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6],
subtitle: '={{ $parameter["operation"] }}',
description: 'Get, add and update data in Postgres',
defaults: {
name: 'Postgres',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
credentials: [
{
name: 'postgres',
required: true,
testedBy: 'postgresConnectionTest',
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'hidden',
noDataExpression: true,
options: [
{
name: 'Database',
value: 'database',
},
],
default: 'database',
},
...database.description,
],
};