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,74 @@
import type { INodeProperties } from 'n8n-workflow';
import * as deleteTable from './deleteTable.operation';
import * as execute 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, execute, 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 SQL',
value: 'execute',
description: 'Execute an SQL',
action: 'Execute SQL',
},
{
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: ['execute'] } } },
{ ...tableRLC, displayOptions: { hide: { operation: ['execute'] } } },
...deleteTable.description,
...execute.description,
...insert.description,
...select.description,
...update.description,
...upsert.description,
];
@@ -0,0 +1,203 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type * as oracleDBTypes from 'oracledb';
import { updateDisplayOptions } from '@utils/utilities';
import type {
OracleDBNodeOptions,
QueriesRunner,
QueryWithValues,
WhereClause,
} from '../../helpers/interfaces';
import {
quoteSqlIdentifier,
addWhereClauses,
getColumnMetaData,
getColumnMap,
getCompatibleValue,
} 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",
},
],
},
{
...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: OracleDBNodeOptions,
pool: oracleDBTypes.Pool,
): Promise<INodeExecutionData[]> {
const queries: QueryWithValues[] = [];
const stmtBatching = nodeOptions.stmtBatching ?? 'independently';
if (stmtBatching !== 'single') {
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 deleteCommand = this.getNodeParameter('deleteCommand', i) as string;
let query = '';
let values: any = [];
const quotedTableName = quoteSqlIdentifier(schema) + '.' + quoteSqlIdentifier(table);
if (deleteCommand === 'drop') {
query = `DECLARE
e_table_missing EXCEPTION;
PRAGMA EXCEPTION_INIT(e_table_missing, -942);
BEGIN
EXECUTE IMMEDIATE ('DROP TABLE ${quotedTableName} PURGE');
EXCEPTION
WHEN e_table_missing THEN NULL;
END;`;
} else if (deleteCommand === 'truncate') {
query = `TRUNCATE TABLE ${quotedTableName}`;
} else if (deleteCommand === 'delete') {
const whereClauses =
((this.getNodeParameter('where', i, []) as IDataObject).values as WhereClause[]) || [];
const combineConditions = this.getNodeParameter('combineConditions', i, 'AND') as string;
const tableSchema = await getColumnMetaData(this.getNode(), pool, schema, table, i);
const columnMetaDataObject = getColumnMap(tableSchema);
[query, values] = addWhereClauses(
`DELETE FROM ${quotedTableName}`,
whereClauses,
combineConditions,
columnMetaDataObject,
);
} else {
throw new NodeOperationError(
this.getNode(),
'Invalid delete command, only drop, delete and truncate are supported ',
{ itemIndex: i },
);
}
const queryWithValues = { query, values };
queries.push(queryWithValues);
}
} else {
const deleteCommand = this.getNodeParameter('deleteCommand', 0) as string;
if (deleteCommand !== 'delete') {
throw new NodeOperationError(
this.getNode(),
'Invalid command for single-mode batching: only DELETE statements are supported.',
{ itemIndex: 0 },
);
}
const schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
const table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
let query: string = '';
let bindDefs: any = [];
const quotedTableName = quoteSqlIdentifier(schema) + '.' + quoteSqlIdentifier(table);
const whereClauses =
((this.getNodeParameter('where', 0, []) as IDataObject).values as WhereClause[]) || [];
const combineConditions = this.getNodeParameter('combineConditions', 0, 'AND') as string;
const tableSchema = await getColumnMetaData(this.getNode(), pool, schema, table);
const columnMetaDataObject = getColumnMap(tableSchema);
[query, bindDefs] = addWhereClauses(
`DELETE FROM ${quotedTableName}`,
whereClauses,
combineConditions,
columnMetaDataObject,
true,
);
const executeManyValues = [];
for (let i = 0; i < items.length; i++) {
const result = [];
const whereClauses =
((this.getNodeParameter('where', i, []) as IDataObject).values as WhereClause[]) || [];
for (const clause of whereClauses) {
const type: string = columnMetaDataObject[clause.column].type;
const value = getCompatibleValue(type, clause.value);
result.push(value);
}
executeManyValues.push(result);
}
nodeOptions.bindDefs = bindDefs;
queries.push({ query, executeManyValues });
}
return await runQueries(queries, items, nodeOptions);
}
@@ -0,0 +1,94 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import type oracledb from 'oracledb';
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
import type {
ExecuteOpBindParam,
OracleDBNodeOptions,
QueriesRunner,
QueryWithValues,
} from '../../helpers/interfaces';
import { getBindParameters } from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Statement',
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 statement to execute. You can use n8n expressions and positional parameters like :1, :2, :3, or named parameters like :name, :ID, etc to refer to the 'Bind Variable Placeholder Values' set in options below.",
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'OracleDB',
},
hint: 'Consider using bind parameters to prevent SQL injection attacks. Add them in the options below',
},
...optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['execute'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
/**
* Query execution function for this node.
*
* This method is called once for every execution of the node during a workflow run.
* It receives input data from the previous node(s) and returns output data to the next node(s).
*
*
* Returns:
* - An array of `INodeExecutionData` objects containing JSON data and optionally binary data, PairedItem,...
*/
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: OracleDBNodeOptions,
_pool?: oracledb.Pool,
): Promise<INodeExecutionData[]> {
const queries: QueryWithValues[] = [];
for (let index = 0; index < items.length; index++) {
let query = this.getNodeParameter('query', index) as string;
// Dynamically replaces placeholders ({{...}}) in SQL queries.
// Ex: SELECT * FROM users WHERE name = '{{ $json["name"] }}'
// to SELECT * FROM users WHERE name = 'Alice'
for (const resolvable of getResolvables(query)) {
query = query.replace(resolvable, this.evaluateExpression(resolvable, index) as string);
}
let values: any = [];
// get list of param objects entered by user
const parameterIDataObjectList =
((this.getNodeParameter('options.params', index, {}) as IDataObject)
.values as ExecuteOpBindParam[]) || [];
if (parameterIDataObjectList.length) {
const { updatedQuery, bindParameters } = getBindParameters(query, parameterIDataObjectList);
query = updatedQuery;
values = bindParameters;
}
queries.push({ query, values });
}
return await runQueries(queries, items, nodeOptions);
}
@@ -0,0 +1,234 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
INode,
} from 'n8n-workflow';
import type * as oracleDBTypes from 'oracledb';
import { updateDisplayOptions } from '@utils/utilities';
import type {
ColumnMap,
QueriesRunner,
OracleDBNodeOptions,
QueryWithValues,
QueryMode,
} from '../../helpers/interfaces';
import {
getInBindParametersForExecute,
getColumnMap,
getOutBindDefsForExecute,
getBindDefsForExecuteMany,
formatItemValues,
quoteSqlIdentifier,
configureTableSchemaUpdater,
getColumnMetaData,
checkItemAgainstSchema,
} from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [
{
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,
},
},
},
...optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['insert'],
},
hide: {
table: [''],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
function getQueryBindParameters(
node: INode,
query: string,
values: oracleDBTypes.BindParameter[],
columnMetaDataObject: ColumnMap,
inputColumns: string[],
outputColumns: string[],
item: IDataObject,
index: number,
) {
checkItemAgainstSchema(node, inputColumns, columnMetaDataObject, item, index);
const [quotedColsArray, replacements, posIndex] = getInBindParametersForExecute(
inputColumns,
columnMetaDataObject,
item,
'insert',
values,
);
const quotedCols = quotedColsArray.join(',');
query = `${query} (${quotedCols}) VALUES (${replacements})`;
if (outputColumns.length > 0) {
const updatedQuery = getOutBindDefsForExecute(
query,
columnMetaDataObject,
outputColumns,
values,
posIndex,
);
query = updatedQuery;
}
return query;
}
/*
* Executes the Node.
*
* @param this Function context (accesses params, helpers).
* @param runQueries Helper function that executes an array of queries.
* @param items Array of input data items.
* @param nodeOptions Node configuration (version, execute options).
* @param pool Database pool object to get Connections to execute.
* @returns Promise which has INodeExecutionData array capturing results.
*/
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: OracleDBNodeOptions,
pool: oracleDBTypes.Pool,
): Promise<INodeExecutionData[]> {
let schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
let table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
let tableSchema = await getColumnMetaData(this.getNode(), pool, schema, table);
const queries: QueryWithValues[] = [];
const stmtBatching = (nodeOptions.stmtBatching as QueryMode) || 'single';
if (stmtBatching === 'single') {
// We assume that the items passed have uniform keys.
// Ex:
// { "id": 1, "name": "Alice" }
// { "id": 2, "name": "Bob" }
// but not
// { "id": 1, "name": "Alice" }
// { "id": 2, "age": 25 }
//
// Also the schema and table are not changing in each item.
const dataMode = this.getNodeParameter('columns.mappingMode', 0) as string;
let item: IDataObject = {};
if (dataMode === 'autoMapInputData') {
item = items[0].json;
} else if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', 0) as IDataObject;
}
const columnMetaDataObject: ColumnMap = getColumnMap(tableSchema);
const inputColumns = Object.keys(item);
let query = `INSERT INTO ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;
let outputColumns = this.getNodeParameter('options.outputColumns', 0, []) as string[];
if (outputColumns.includes('*')) outputColumns = Object.keys(columnMetaDataObject);
query = getBindDefsForExecuteMany(
this.getNode(),
query,
columnMetaDataObject,
inputColumns,
outputColumns,
item,
nodeOptions,
);
const executeManyValues = [];
for (let i = 0; i < items.length; i++) {
if (dataMode === 'autoMapInputData') {
item = items[i].json;
}
if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', i) as IDataObject;
}
const newItem = formatItemValues(item, columnMetaDataObject);
executeManyValues.push(newItem);
}
queries.push({ query, executeManyValues, outputColumns });
} else {
const updateTableSchema = configureTableSchemaUpdater(this.getNode(), schema, table);
for (let i = 0; i < items.length; i++) {
schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
const dataMode = this.getNodeParameter('columns.mappingMode', i) as string;
let item: IDataObject = {};
if (dataMode === 'autoMapInputData') {
item = items[i].json;
// Column refresh is needed only for 'autoMapInputData'
tableSchema = await updateTableSchema(pool, tableSchema, schema, table, i);
} else if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', i) as IDataObject;
}
const columnMetaDataObject = getColumnMap(tableSchema);
const inputColumns = Object.keys(item);
let query = `INSERT INTO ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;
let outputColumns = this.getNodeParameter('options.outputColumns', i, []) as string[];
const bindParams: oracleDBTypes.BindParameter[] = [];
if (outputColumns.includes('*')) outputColumns = Object.keys(columnMetaDataObject);
query = getQueryBindParameters(
this.getNode(),
query,
bindParams,
columnMetaDataObject,
inputColumns,
outputColumns,
item,
i,
);
queries.push({ query, values: bindParams, outputColumns });
}
}
return await runQueries(queries, items, nodeOptions);
}
@@ -0,0 +1,156 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import type * as oracleDBTypes from 'oracledb';
import { updateDisplayOptions } from '@utils/utilities';
import type {
OracleDBNodeOptions,
QueriesRunner,
QueryWithValues,
SortRule,
WhereClause,
} from '../../helpers/interfaces';
import {
addSortRules,
addWhereClauses,
quoteSqlIdentifier,
getColumnMap,
getColumnMetaData,
} 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: ['database'],
operation: ['select'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
resource: ['database'],
operation: ['select'],
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: OracleDBNodeOptions,
pool: oracleDBTypes.Pool,
): Promise<INodeExecutionData[]> {
const queries: QueryWithValues[] = [];
const conn = await pool.getConnection();
const isCDBSupported = conn.oracleServerVersion >= 1200000000;
await conn.close();
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 tableSchema = await getColumnMetaData(this.getNode(), pool, schema, table, i);
const columnMetaDataObject = getColumnMap(tableSchema);
let values: any = [];
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];
let query = '';
let innerQuery = outputColumns.includes('*')
? `SELECT * FROM ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`
: `SELECT ${outputColumns.map(quoteSqlIdentifier).join(',')} FROM ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;
// Add WHERE clause
const whereClauses =
((this.getNodeParameter('where', i, []) as IDataObject).values as WhereClause[]) || [];
const combineConditions = this.getNodeParameter('combineConditions', i, 'AND') as string;
[innerQuery, values] = addWhereClauses(
innerQuery,
whereClauses,
combineConditions,
columnMetaDataObject,
);
// Add ORDER BY if needed
const sortRules =
((this.getNodeParameter('sort', i, []) as IDataObject).values as SortRule[]) || [];
innerQuery = addSortRules(innerQuery, sortRules);
// Handle LIMIT / pagination
const returnAll = this.getNodeParameter('returnAll', i, false);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i, 50);
if (isCDBSupported) {
// Oracle 12c+ (FETCH FIRST)
query += `${innerQuery} FETCH FIRST ${limit} ROWS ONLY`;
} else {
if (sortRules.length > 0 || whereClauses.length > 0) {
// Wrap inner query to preserve WHERE + ORDER BY
query = `SELECT * FROM (${innerQuery}) WHERE ROWNUM <= ${limit}`;
} else {
// No ORDER BY or WHERE: safe to append ROWNUM inline
query = `${innerQuery} WHERE ROWNUM <= ${limit}`;
}
}
} else {
// return all: no limit
query = innerQuery;
}
const queryWithValues = { query, values };
queries.push(queryWithValues);
}
return await runQueries(queries, items, nodeOptions);
}
@@ -0,0 +1,244 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type * as oracleDBTypes from 'oracledb';
import { updateDisplayOptions } from '@utils/utilities';
import type {
ColumnMap,
OracleDBNodeOptions,
QueryMode,
QueriesRunner,
QueryWithValues,
} from '../../helpers/interfaces';
import {
getCompatibleValue,
getInBindParametersForExecute,
getOutBindDefsForExecute,
getBindDefsForExecuteMany,
getColumnMap,
configureTableSchemaUpdater,
getColumnMetaData,
quoteSqlIdentifier,
} from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [
{
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,
},
},
},
...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: OracleDBNodeOptions,
pool: oracleDBTypes.Pool,
): Promise<INodeExecutionData[]> {
let schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
let table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
let tableSchema = await getColumnMetaData(this.getNode(), pool, schema, table);
const queries: QueryWithValues[] = [];
const stmtBatching = (nodeOptions.stmtBatching as QueryMode) || 'single';
if (stmtBatching === 'single') {
const dataMode = this.getNodeParameter('columns.mappingMode', 0) as string;
let item: IDataObject = {};
if (dataMode === 'autoMapInputData') {
item = items[0].json;
} else if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', 0) as IDataObject;
}
const columnMetaDataObject: ColumnMap = getColumnMap(tableSchema);
// where clause column
const columnsToMatchOn: string[] = this.getNodeParameter(
'columns.matchingColumns',
0,
) as string[];
const updateColumns = Object.keys(item).filter((column) => !columnsToMatchOn.includes(column));
if (!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.",
);
}
let query = `UPDATE ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;
let outputColumns = this.getNodeParameter('options.outputColumns', 0, []) as string[];
if (outputColumns.includes('*')) outputColumns = Object.keys(columnMetaDataObject);
query = getBindDefsForExecuteMany(
this.getNode(),
query,
columnMetaDataObject,
updateColumns,
outputColumns,
item,
nodeOptions,
'update',
columnsToMatchOn,
);
const executeManyValues = [];
const keysOrder = [...updateColumns, ...columnsToMatchOn];
for (let i = 0; i < items.length; i++) {
if (dataMode === 'autoMapInputData') {
item = items[i].json;
} else if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', i) as IDataObject;
}
const result = [];
for (const key of keysOrder) {
const type = columnMetaDataObject[key].type;
let value: any = item[key];
value = getCompatibleValue(type, value);
result.push(value);
}
executeManyValues.push(result);
}
queries.push({ query, executeManyValues, outputColumns });
} else {
const updateTableSchema = configureTableSchemaUpdater(this.getNode(), schema, table);
for (let index = 0; index < items.length; index++) {
schema = this.getNodeParameter('schema', index, undefined, {
extractValue: true,
}) as string;
table = this.getNodeParameter('table', index, undefined, {
extractValue: true,
}) as string;
const dataMode = this.getNodeParameter('columns.mappingMode', index) as string;
let item: IDataObject = {};
if (dataMode === 'autoMapInputData') {
item = items[index].json;
// Column refresh is needed only for 'autoMapInputData'
tableSchema = await updateTableSchema(pool, tableSchema, schema, table, index);
} else if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', index) as IDataObject;
}
// where clause column
const columnsToMatchOn: string[] = this.getNodeParameter(
'columns.matchingColumns',
index,
) as string[];
const columnMetaDataObject = getColumnMap(tableSchema);
const updateColumns = Object.keys(item).filter(
(column) => !columnsToMatchOn.includes(column),
);
if (!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.",
);
}
if (Object.keys(item).length === columnsToMatchOn.length) {
// Only match column exists, nothing to update
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 bindParams: oracleDBTypes.BindParameter[] = []; // bindParameters
let [quotedColsArray, _replacements, posIndex] = getInBindParametersForExecute(
updateColumns,
columnMetaDataObject,
item,
'update',
bindParams,
);
let query = `UPDATE ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)} SET ${quotedColsArray.join(',')}`;
if (columnsToMatchOn.length > 0) {
[quotedColsArray, _replacements, posIndex] = getInBindParametersForExecute(
columnsToMatchOn,
columnMetaDataObject,
item,
'update',
bindParams,
posIndex,
);
const condition = quotedColsArray.join(' AND ');
query += ` WHERE ${condition}`;
}
let outputColumns = this.getNodeParameter('options.outputColumns', index, []) as string[];
if (outputColumns.includes('*')) outputColumns = Object.keys(columnMetaDataObject);
if (outputColumns.length > 0) {
const updatedQuery = getOutBindDefsForExecute(
query,
columnMetaDataObject,
outputColumns,
bindParams,
posIndex,
);
query = updatedQuery;
}
queries.push({ query, values: bindParams, outputColumns });
}
}
return await runQueries(queries, items, nodeOptions);
}
@@ -0,0 +1,276 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type * as oracleDBTypes from 'oracledb';
import { updateDisplayOptions } from '@utils/utilities';
import type {
ColumnInfo,
ColumnMap,
OracleDBNodeOptions,
QueriesRunner,
QueryWithValues,
} from '../../helpers/interfaces';
import {
configureTableSchemaUpdater,
getColumnMap,
getCompatibleValue,
getInBindParametersForSourceSelect,
getOnClauseFromColumns,
getInsertClauseAndBinds,
getUpdateSetClause,
getColumnMetaData,
getOutBindDefsForExecute,
quoteSqlIdentifier,
} from '../../helpers/utils';
import { optionsCollection } from '../common.descriptions';
const properties: INodeProperties[] = [
{
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,
},
},
},
...optionsCollection,
];
const displayOptions = {
show: {
resource: ['database'],
operation: ['upsert'],
},
hide: {
table: [''],
},
};
function getQueryAndOutputColumns(
ctx: IExecuteFunctions,
items: INodeExecutionData[],
item: IDataObject,
schema: string,
table: string,
tableSchema: ColumnInfo[],
bindParams: oracleDBTypes.BindParameter[],
bindDefs: oracleDBTypes.BindDefinition[] | null,
index: number,
executeManyValues: any[] | null = null,
): [string, string[]] {
const columnMetaDataObject: ColumnMap = getColumnMap(tableSchema);
const columnsToMatchOn: string[] = ctx.getNodeParameter(
'columns.matchingColumns',
index,
) as string[];
if (columnsToMatchOn.length === 0) {
throw new NodeOperationError(
ctx.getNode(),
'No matching columns configured. Please define at least one column to match on.',
);
}
if (item[columnsToMatchOn[0]] === undefined) {
throw new NodeOperationError(
ctx.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 (Object.keys(item).length === columnsToMatchOn.length) {
// Only match column exists, nothing to update/insert
throw new NodeOperationError(
ctx.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.",
);
}
const updateColumns = Object.keys(item).filter((column) => !columnsToMatchOn.includes(column));
const inputColumns = Object.keys(item);
const [sourceSelect, posIndex1] = getInBindParametersForSourceSelect(
columnsToMatchOn,
columnMetaDataObject,
item,
bindParams,
bindDefs,
0,
);
const onClause = getOnClauseFromColumns(columnsToMatchOn);
const [updateSetClause, posIndex2] = getUpdateSetClause(
updateColumns,
columnMetaDataObject,
item,
bindParams,
bindDefs,
posIndex1,
);
const [insertColsStr, insertValsStr, posIndex3] = getInsertClauseAndBinds(
inputColumns,
columnMetaDataObject,
item,
bindParams,
bindDefs,
posIndex2,
);
let query = `MERGE INTO ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)} t
USING (SELECT ${sourceSelect} FROM dual) s
ON (${onClause})
WHEN MATCHED THEN
UPDATE SET ${updateSetClause}
WHEN NOT MATCHED THEN
INSERT (${insertColsStr}) VALUES (${insertValsStr})
`;
let outputColumns = ctx.getNodeParameter('options.outputColumns', 0, []) as string[];
if (outputColumns.includes('*')) outputColumns = Object.keys(columnMetaDataObject);
if (outputColumns.length > 0) {
query = getOutBindDefsForExecute(
query,
columnMetaDataObject,
outputColumns,
bindDefs ?? bindParams,
posIndex3,
);
}
if (executeManyValues) {
const keysOrder = [...columnsToMatchOn, ...updateColumns, ...inputColumns];
for (let i = 0; i < items.length; i++) {
const dataMode = ctx.getNodeParameter('columns.mappingMode', i) as string;
if (dataMode === 'autoMapInputData') {
item = items[i].json;
}
if (dataMode === 'defineBelow') {
item = ctx.getNodeParameter('columns.value', i) as IDataObject;
}
const result = [];
for (const key of keysOrder) {
const type = columnMetaDataObject[key].type;
const value = getCompatibleValue(type, item[key]);
result.push(value);
}
executeManyValues.push(result);
}
}
return [query, outputColumns];
}
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
runQueries: QueriesRunner,
items: INodeExecutionData[],
nodeOptions: OracleDBNodeOptions,
pool: oracleDBTypes.Pool,
): Promise<INodeExecutionData[]> {
const stmtBatching = nodeOptions.stmtBatching ?? 'single';
const queries: QueryWithValues[] = [];
let item: IDataObject = {};
let schema = this.getNodeParameter('schema', 0, undefined, {
extractValue: true,
}) as string;
let table = this.getNodeParameter('table', 0, undefined, {
extractValue: true,
}) as string;
let tableSchema = await getColumnMetaData(this.getNode(), pool, schema, table);
let dataMode = this.getNodeParameter('columns.mappingMode', 0) as string;
if (stmtBatching === 'single') {
const executeManyValues: oracleDBTypes.BindParameters[] = [];
const bindDefs: oracleDBTypes.BindDefinition[] = [];
if (dataMode === 'autoMapInputData') {
item = items[0].json;
} else if (dataMode === 'defineBelow') {
item = this.getNodeParameter('columns.value', 0) as IDataObject;
}
const [query, outputColumns] = getQueryAndOutputColumns(
this,
items,
item,
schema,
table,
tableSchema,
[],
bindDefs,
0,
executeManyValues,
);
nodeOptions.bindDefs = bindDefs;
queries.push({ query, executeManyValues, outputColumns });
} else {
const updateTableSchema = configureTableSchemaUpdater(this.getNode(), schema, table);
for (let i = 0; i < items.length; i++) {
dataMode = this.getNodeParameter('columns.mappingMode', i) as string;
schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
}) as string;
table = this.getNodeParameter('table', i, undefined, {
extractValue: true,
}) as string;
if (dataMode === 'autoMapInputData') {
item = items[i].json;
// Column refresh is needed only for 'autoMapInputData'
tableSchema = await updateTableSchema(pool, tableSchema, schema, table, i);
} else {
item = this.getNodeParameter('columns.value', i) as IDataObject;
}
const bindParams: oracleDBTypes.BindParameter[] = []; // bindParameters
const [query, outputColumns] = getQueryAndOutputColumns(
this,
items,
item,
schema,
table,
tableSchema,
bindParams,
null,
i,
null,
);
queries.push({ query, values: bindParams, outputColumns });
}
}
return await runQueries(queries, items, nodeOptions);
}