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
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:
@@ -0,0 +1,29 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { router } from './actions/router';
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { credentialTest, listSearch, loadOptions, resourceMapping } from './methods';
|
||||
|
||||
//oracleDBTypes.fetchAsString = [oracleDBTypes.CLOB]; TBD
|
||||
export class OracleSql implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = { credentialTest, listSearch, loadOptions, resourceMapping };
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
return await router.call(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
const stmtBatchOptions = [
|
||||
{
|
||||
name: 'Single Statement',
|
||||
value: 'single',
|
||||
description: 'A single Statement for all incoming items',
|
||||
},
|
||||
{
|
||||
name: 'Independently',
|
||||
value: 'independently',
|
||||
description: 'Execute one Statement per incoming item of the run',
|
||||
},
|
||||
{
|
||||
name: 'Transaction',
|
||||
value: 'transaction',
|
||||
description:
|
||||
'Execute all Statements in a transaction, if a failure occurs, all changes are rolled back',
|
||||
},
|
||||
];
|
||||
|
||||
export const optionsCollection: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Auto Commit',
|
||||
name: 'autoCommit',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether this property is true, then the transaction in the current connection is automatically committed at the end of statement execution',
|
||||
},
|
||||
{
|
||||
displayName: 'Bind Variable Placeholder Values',
|
||||
name: 'params',
|
||||
placeholder: 'Add Parameter',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add another Parameter',
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['execute'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Enter the values for the bind parameters used in the statement',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Bind Name or Number',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A bind variable placeholder identifier or numeral',
|
||||
placeholder:
|
||||
'e.g. ``dept_id`` and ``dept_name`` are the two bind variables placeholders in this SQL statement',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Bind Direction',
|
||||
name: 'bindDirection',
|
||||
type: 'options',
|
||||
default: 'in',
|
||||
required: true,
|
||||
description:
|
||||
'Specify whether data values bound to SQL or PL/SQL bind parameters are passed into, or out from, the database',
|
||||
options: [
|
||||
{ name: 'IN', value: 'in' },
|
||||
{ name: 'OUT', value: 'out' },
|
||||
{ name: 'IN-OUT', value: 'inout' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Data Type',
|
||||
name: 'datatype',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: 'string',
|
||||
options: [
|
||||
{ name: 'BLOB', value: 'blob' },
|
||||
{ name: 'Boolean', value: 'boolean' },
|
||||
{ name: 'Date', value: 'date' },
|
||||
{ name: 'JSON', value: 'json' },
|
||||
{ name: 'Number', value: 'number' },
|
||||
{ name: 'SparseVector', value: 'sparse' },
|
||||
{ name: 'String', value: 'string' },
|
||||
{ name: 'Vector', value: 'vector' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Value (String)',
|
||||
name: 'valueString',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['string'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value (Number)',
|
||||
name: 'valueNumber',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['number'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value (Date)',
|
||||
name: 'valueDate',
|
||||
type: 'dateTime',
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['date'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value (Boolean)',
|
||||
name: 'valueBoolean',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['boolean'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value (JSON)',
|
||||
name: 'valueJson',
|
||||
type: 'json',
|
||||
default: '{}',
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value (VECTOR)',
|
||||
name: 'valueVector',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['vector'],
|
||||
},
|
||||
},
|
||||
placeholder: '[1.2, 3.4, 5.6]',
|
||||
description: 'A JSON array of dimension values',
|
||||
},
|
||||
{
|
||||
displayName: 'Value (BLOB)',
|
||||
name: 'valueBlob',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['blob'],
|
||||
},
|
||||
},
|
||||
placeholder: '{ "type": "Buffer", "data": [98,10] }',
|
||||
description: 'A Binary data',
|
||||
},
|
||||
{
|
||||
displayName: 'Value (Sparse Vector)',
|
||||
name: 'valueSparse',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
datatype: ['sparse'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Dimensions',
|
||||
name: 'dimensions',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Total number of dimensions',
|
||||
},
|
||||
{
|
||||
displayName: 'Indices',
|
||||
name: 'indices',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
placeholder: '[0, 2, 5]',
|
||||
description: 'A JSON array of indices, e.g., [0, 2, 5]',
|
||||
},
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
placeholder: '[1.2, 3.4, 5.6]',
|
||||
description: 'A JSON array of values matching indices',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Parse for IN Statement',
|
||||
name: 'parseInStatement',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: false,
|
||||
hint: 'If "Yes" the "Value" field should be a string of comma-separated values. i.e: 1,2,3 or str1,str2,str3',
|
||||
options: [
|
||||
{ name: 'No', value: false },
|
||||
{ name: 'Yes', value: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Fetch Array Size',
|
||||
name: 'fetchArraySize',
|
||||
type: 'number',
|
||||
default: 100,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['execute', 'select'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'This property is a number that sets the size of an internal buffer used for fetching query rows from Oracle Database. Changing it may affect query performance but does not affect how many rows are returned to the application.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Number of Rows to Prefetch',
|
||||
name: 'prefetchRows',
|
||||
type: 'number',
|
||||
default: 2,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['execute', 'select'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
},
|
||||
description:
|
||||
'This property is a query tuning option to set the number of additional rows the underlying Oracle driver fetches during the internal initial statement execution phase of a query',
|
||||
},
|
||||
{
|
||||
// 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': ['insert', 'select', 'update', 'upsert'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Numbers As String',
|
||||
name: 'largeNumbersOutputAsString',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the numbers should be retrieved as string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['execute', 'select'],
|
||||
},
|
||||
},
|
||||
hint: 'Applies to NUMBER, FLOAT, LONG type columns only',
|
||||
},
|
||||
{
|
||||
displayName: 'Statement Batching',
|
||||
name: 'stmtBatching',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: stmtBatchOptions,
|
||||
default: 'single',
|
||||
displayOptions: {
|
||||
show: { '/operation': ['update', 'insert', 'upsert'] },
|
||||
},
|
||||
description: 'The way queries should be sent to the database',
|
||||
},
|
||||
{
|
||||
displayName: 'Statement Batching',
|
||||
name: 'stmtBatching',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: stmtBatchOptions,
|
||||
default: 'independently',
|
||||
displayOptions: {
|
||||
show: { '/operation': ['deleteTable'] },
|
||||
},
|
||||
description: 'The way queries should be sent to the database',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Important: Single Statement mode works only for the first item',
|
||||
name: 'stmtBatchingNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/options.stmtBatching': ['single'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const schemaRLC: INodeProperties = {
|
||||
displayName: 'Schema',
|
||||
name: 'schema',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
placeholder: 'e.g. scott',
|
||||
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: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
default: 'equal',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'json',
|
||||
description: 'Parameters to pass to the tool as JSON or string',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
condition: ['IS NULL', 'IS NOT NULL'],
|
||||
},
|
||||
},
|
||||
default: '{"key": "val"}',
|
||||
placeholder: '{ "key": "value" }',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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 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);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { AllEntities, Entity } from 'n8n-workflow';
|
||||
|
||||
type OracleDBMap = {
|
||||
database: 'deleteTable' | 'execute' | 'insert' | 'select' | 'update' | 'upsert';
|
||||
};
|
||||
|
||||
export type OracleDBType = AllEntities<OracleDBMap>;
|
||||
|
||||
export type OracleDatabaseType = Entity<OracleDBMap, 'database'>;
|
||||
|
||||
export function isOracleDBOperation(op: string): op is OracleDBMap['database'] {
|
||||
return ['deleteTable', 'execute', 'insert', 'select', 'update', 'upsert'].includes(op);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import * as database from './database/Database.resource';
|
||||
import type { OracleDBType } from './node.type';
|
||||
import { isOracleDBOperation } from './node.type';
|
||||
import type { OracleDBNodeCredentials, OracleDBNodeOptions } from '../helpers/interfaces';
|
||||
import { configureQueryRunner } from '../helpers/utils';
|
||||
import { configureOracleDB } from '../transport';
|
||||
|
||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
let returnData: INodeExecutionData[] = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
const resource = this.getNodeParameter<OracleDBType>('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
if (!isOracleDBOperation(operation)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The operation "${operation}" is not a valid value!`,
|
||||
);
|
||||
}
|
||||
|
||||
const credentials = await this.getCredentials<OracleDBNodeCredentials>('oracleDBApi');
|
||||
const options = this.getNodeParameter('options', 0, {}) as OracleDBNodeOptions;
|
||||
const node = this.getNode();
|
||||
options.nodeVersion = node.typeVersion;
|
||||
options.operation = operation;
|
||||
options.autoCommit = options.autoCommit ?? true;
|
||||
|
||||
const pool = await configureOracleDB.call(this, credentials, options);
|
||||
const runQueries = configureQueryRunner.call(this, this.getNode(), this.continueOnFail(), pool);
|
||||
const oracleDBNodeData: OracleDBType = {
|
||||
resource,
|
||||
operation,
|
||||
};
|
||||
|
||||
switch (oracleDBNodeData.resource) {
|
||||
case 'database':
|
||||
returnData = await database[oracleDBNodeData.operation].execute.call(
|
||||
this,
|
||||
runQueries,
|
||||
items,
|
||||
options,
|
||||
pool,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The operation "${operation}" is not supported!`,
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'select' && items.length > 1 && !node.executeOnce) {
|
||||
this.addExecutionHints({
|
||||
message: `This node ran ${items.length} times, once for each input item. To run for the first item only, enable 'execute once' in the node settings`,
|
||||
location: 'outputPane',
|
||||
});
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* 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: 'Oracle Database',
|
||||
name: 'oracleDatabase',
|
||||
icon: 'file:oracle.svg',
|
||||
group: ['input'],
|
||||
version: [1],
|
||||
subtitle: '={{ $parameter["operation"] }}',
|
||||
description: 'Get, add and update data in Oracle database',
|
||||
defaults: {
|
||||
name: 'Oracle Database',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
usableAsTool: true,
|
||||
credentials: [
|
||||
{
|
||||
name: 'oracleDBApi',
|
||||
required: true,
|
||||
testedBy: 'oracleDBConnectionTest',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'hidden',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Database',
|
||||
value: 'database',
|
||||
},
|
||||
],
|
||||
default: 'database',
|
||||
},
|
||||
...database.description,
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { DateTime } from 'luxon';
|
||||
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import type * as oracleDBTypes from 'oracledb';
|
||||
|
||||
export type QueryMode = 'single' | 'transaction' | 'independently';
|
||||
|
||||
export type ObjectQueryValue = Extract<oracleDBTypes.BindParameters, Record<string, unknown>>;
|
||||
|
||||
// For execute
|
||||
export type QueryValue =
|
||||
| ObjectQueryValue // named binds in object form
|
||||
| oracleDBTypes.BindParameters; // positional binds in array form
|
||||
|
||||
// A query string along with its bind values.
|
||||
export type QueryWithValues = {
|
||||
query: string;
|
||||
values?: QueryValue; // For execute
|
||||
executeManyValues?: QueryValue[]; // for executeMany
|
||||
outputColumns?: string[]; // RETURNING INTO columns in sql string
|
||||
};
|
||||
|
||||
export type WhereClause = { column: string; condition: string; value: any };
|
||||
export type SortRule = { column: string; direction: string };
|
||||
export type ColumnInfo = {
|
||||
columnName: string;
|
||||
dataType: string;
|
||||
isNullable: boolean;
|
||||
udtName?: string;
|
||||
columnDefault?: string | null;
|
||||
isGenerated?: 'ALWAYS' | 'NEVER';
|
||||
identityGeneration?: 'ALWAYS' | 'NEVER';
|
||||
maxSize: number;
|
||||
};
|
||||
|
||||
export type QueriesRunner = (
|
||||
queries: QueryWithValues[],
|
||||
items: INodeExecutionData[],
|
||||
options: IDataObject,
|
||||
) => Promise<INodeExecutionData[]>;
|
||||
|
||||
export type OracleDBNodeOptions = {
|
||||
nodeVersion?: number;
|
||||
operation?: string;
|
||||
|
||||
// Connection options
|
||||
poolPingInterval?: number;
|
||||
poolPingTimeout?: number;
|
||||
stmtCacheSize?: number;
|
||||
poolMax?: number;
|
||||
poolMin?: number;
|
||||
poolIncrement?: number;
|
||||
|
||||
// Execute options
|
||||
autoCommit?: boolean;
|
||||
bindDefs?: oracleDBTypes.BindDefinition[];
|
||||
batchErrors?: boolean;
|
||||
fetchArraySize?: number;
|
||||
keepInStmtCache?: boolean;
|
||||
maxRows?: number;
|
||||
prefetchRows?: number;
|
||||
|
||||
// n8n options
|
||||
largeNumbersOutputAsString?: boolean; // bigInt
|
||||
outputColumns?: string[];
|
||||
stmtBatching?: QueryMode;
|
||||
executeManyOptions?: oracleDBTypes.ExecuteManyOptions;
|
||||
};
|
||||
|
||||
export type OracleDBNodeCredentials = {
|
||||
connectionString: string | undefined;
|
||||
connectionClass?: string;
|
||||
connectTimeout?: number;
|
||||
useThickMode: boolean;
|
||||
useSSL: boolean;
|
||||
expireTime?: number;
|
||||
maxLifetimeSession: number;
|
||||
password: string | undefined;
|
||||
poolTimeout: number;
|
||||
poolMin: number;
|
||||
poolMax: number;
|
||||
poolIncrement: number;
|
||||
privilege?: number;
|
||||
sslServerCertDN?: string;
|
||||
sslServerDNMatch?: boolean;
|
||||
sslAllowWeakDNMatch?: boolean;
|
||||
transportConnectTimeout?: number;
|
||||
user: string | undefined;
|
||||
walletPassword?: string | undefined;
|
||||
walletContent?: string | undefined;
|
||||
};
|
||||
|
||||
export type ColumnDefinition = {
|
||||
type: string;
|
||||
nullable: boolean;
|
||||
maxSize: number;
|
||||
};
|
||||
|
||||
export type ColumnMap = {
|
||||
[key: string]: ColumnDefinition;
|
||||
};
|
||||
|
||||
// shared fields
|
||||
type BaseBindFields = {
|
||||
name: string; // bind param name
|
||||
parseInStatement: boolean;
|
||||
bindDirection: 'in' | 'out' | 'inout'; // restrict to known directions
|
||||
};
|
||||
|
||||
// discriminated union
|
||||
export type ExecuteOpBindParam =
|
||||
| (BaseBindFields & { datatype: 'string'; valueString: string })
|
||||
| (BaseBindFields & { datatype: 'number'; valueNumber: number })
|
||||
| (BaseBindFields & { datatype: 'boolean'; valueBoolean: boolean })
|
||||
| (BaseBindFields & { datatype: 'date'; valueDate: string | Date | DateTime | null })
|
||||
| (BaseBindFields & { datatype: 'json'; valueJson: Record<string, unknown> | null })
|
||||
| (BaseBindFields & { datatype: 'vector'; valueVector: number[] | null })
|
||||
| (BaseBindFields & { datatype: 'blob'; valueBlob: Buffer | null })
|
||||
| (BaseBindFields & {
|
||||
datatype: 'sparse';
|
||||
valueSparse: {
|
||||
dimensions: number;
|
||||
indices: number[];
|
||||
values: number[];
|
||||
};
|
||||
});
|
||||
|
||||
// Definition of row returned for column information.
|
||||
export interface TableColumnRow {
|
||||
COLUMN_NAME: string;
|
||||
DATA_TYPE: string;
|
||||
DATA_LENGTH: number;
|
||||
CHAR_LENGTH: number;
|
||||
DEFAULT_LENGTH: number | null;
|
||||
NULLABLE: 'Y' | 'N';
|
||||
IDENTITY_COLUMN?: 'YES' | 'NO'; // only present in 12c+
|
||||
HAS_DEFAULT: 'YES' | 'NO';
|
||||
CONSTRAINT_TYPES?: string | null;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
import type {
|
||||
ICredentialsDecrypted,
|
||||
ICredentialTestFunctions,
|
||||
INodeCredentialTestResult,
|
||||
} from 'n8n-workflow';
|
||||
import type * as oracleDBTypes from 'oracledb';
|
||||
|
||||
import type { OracleDBNodeCredentials } from '../helpers/interfaces';
|
||||
import { configureOracleDB } from '../transport';
|
||||
|
||||
export async function oracleDBConnectionTest(
|
||||
this: ICredentialTestFunctions,
|
||||
credential: ICredentialsDecrypted,
|
||||
): Promise<INodeCredentialTestResult> {
|
||||
const credentials = credential.data as OracleDBNodeCredentials;
|
||||
|
||||
let pool: oracleDBTypes.Pool;
|
||||
|
||||
try {
|
||||
pool = await configureOracleDB.call(this, credentials, {});
|
||||
const conn = await pool.getConnection();
|
||||
await conn.close();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
status: 'Error',
|
||||
message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'OK',
|
||||
message: 'Connection successful!',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * as credentialTest from './credentialTest';
|
||||
export * as listSearch from './listSearch';
|
||||
export * as loadOptions from './loadOptions';
|
||||
export * as resourceMapping from './resourceMapping';
|
||||
@@ -0,0 +1,86 @@
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
||||
import * as oracleDBTypes from 'oracledb';
|
||||
|
||||
import type { OracleDBNodeCredentials } from '../helpers/interfaces';
|
||||
import { configureOracleDB } from '../transport';
|
||||
|
||||
export async function schemaSearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const credentials = await this.getCredentials<OracleDBNodeCredentials>('oracleDBApi');
|
||||
const options = { nodeVersion: this.getNode().typeVersion };
|
||||
|
||||
const pool: oracleDBTypes.Pool = await configureOracleDB.call(this, credentials, options);
|
||||
|
||||
let conn: oracleDBTypes.Connection | undefined;
|
||||
|
||||
try {
|
||||
conn = await pool.getConnection();
|
||||
|
||||
const response = await conn.execute<{ USERNAME: string }>(
|
||||
'SELECT username FROM all_users',
|
||||
[],
|
||||
{
|
||||
outFormat: oracleDBTypes.OUT_FORMAT_OBJECT,
|
||||
},
|
||||
);
|
||||
|
||||
const results =
|
||||
response.rows?.map((schema) => ({
|
||||
name: schema.USERNAME,
|
||||
value: schema.USERNAME,
|
||||
})) ?? [];
|
||||
|
||||
return { results };
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), `Failed to fetch schemas: ${error.message}`);
|
||||
} finally {
|
||||
if (conn) {
|
||||
await conn.close(); // Ensure connection is closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function tableSearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const credentials = await this.getCredentials<OracleDBNodeCredentials>('oracleDBApi');
|
||||
const options = { nodeVersion: this.getNode().typeVersion };
|
||||
|
||||
const pool: oracleDBTypes.Pool = await configureOracleDB.call(this, credentials, options);
|
||||
|
||||
let conn: oracleDBTypes.Connection | undefined;
|
||||
|
||||
try {
|
||||
// Get the connection from the pool
|
||||
conn = await pool.getConnection();
|
||||
|
||||
// Retrieve the schema parameter
|
||||
const schema = this.getNodeParameter('schema', 0, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
// Execute the SQL query to fetch table names for the given schema
|
||||
const response = await conn.execute<{ TABLE_NAME: string }>(
|
||||
'SELECT table_name FROM all_tables WHERE owner = (:1)',
|
||||
[schema],
|
||||
{
|
||||
outFormat: oracleDBTypes.OUT_FORMAT_OBJECT, // Ensure that the response is in object format
|
||||
},
|
||||
);
|
||||
|
||||
// Map through the response.rows and format them
|
||||
const results =
|
||||
response.rows?.map((table) => ({
|
||||
name: table.TABLE_NAME,
|
||||
value: table.TABLE_NAME,
|
||||
})) ?? []; // Handle the case where rows might be undefined or empty
|
||||
|
||||
// Return the results in the required format
|
||||
return { results };
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), `Failed to fetch tables: ${error.message}`);
|
||||
} finally {
|
||||
// Ensure the connection is always closed
|
||||
if (conn) {
|
||||
await conn.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
||||
import type * as oracleDBTypes from 'oracledb';
|
||||
|
||||
import type { OracleDBNodeCredentials } from '../helpers/interfaces';
|
||||
import { getColumnMetaData } from '../helpers/utils';
|
||||
import { configureOracleDB } from '../transport';
|
||||
|
||||
export async function getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const credentials = await this.getCredentials<OracleDBNodeCredentials>('oracleDBApi');
|
||||
const options = { nodeVersion: this.getNode().typeVersion };
|
||||
|
||||
const pool: oracleDBTypes.Pool = await configureOracleDB.call(this, credentials, options);
|
||||
|
||||
const schema = this.getNodeParameter('schema', 0, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const table = this.getNodeParameter('table', 0, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const columns = await getColumnMetaData(this.getNode(), pool, schema, table);
|
||||
|
||||
return columns.map((column) => ({
|
||||
name: column.columnName,
|
||||
value: column.columnName,
|
||||
description: `Type: ${column.dataType.toUpperCase()}, Nullable: ${column.isNullable}`,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getColumnsMultiOptions(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData = await getColumns.call(this);
|
||||
const returnAll = { name: '*', value: '*', description: 'All columns' };
|
||||
return [returnAll, ...returnData];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ILoadOptionsFunctions, ResourceMapperFields, FieldType } from 'n8n-workflow';
|
||||
|
||||
import type { OracleDBNodeCredentials } from '../helpers/interfaces';
|
||||
import { getColumnMetaData, mapDbType } from '../helpers/utils';
|
||||
import { configureOracleDB } from '../transport';
|
||||
|
||||
export async function getMappingColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<ResourceMapperFields> {
|
||||
const credentials = await this.getCredentials<OracleDBNodeCredentials>('oracleDBApi');
|
||||
|
||||
const pool = await configureOracleDB.call(this, credentials);
|
||||
|
||||
const schema = this.getNodeParameter('schema', 0, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const table = this.getNodeParameter('table', 0, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
const columns = await getColumnMetaData(this.getNode(), pool, schema, table);
|
||||
const fields = columns.map((col) => {
|
||||
const type = mapDbType(col.dataType).n8nType as FieldType;
|
||||
const nullable = col.isNullable;
|
||||
const hasDefault = col.columnDefault === 'YES';
|
||||
const isGenerated = col.isGenerated === 'ALWAYS';
|
||||
|
||||
return {
|
||||
id: col.columnName,
|
||||
displayName: col.columnName,
|
||||
required: !nullable && !hasDefault && !isGenerated,
|
||||
display: true,
|
||||
type,
|
||||
defaultMatch: true,
|
||||
};
|
||||
});
|
||||
return { fields };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#F00" fill-rule="evenodd" d="M7.957359,18.9123664 C4.11670252,18.9123664 1,15.803458 1,11.9617373 C1,8.12000773 4.11670252,5 7.957359,5 L16.0437948,5 C19.8855156,5 23,8.12000773 23,11.9617373 C23,15.803458 19.8855156,18.9123664 16.0437948,18.9123664 L7.957359,18.9123664 L7.957359,18.9123664 Z M15.8639176,16.4585488 C18.352201,16.4585488 20.3674397,14.448858 20.3674397,11.9617373 C20.3674397,9.47460595 18.352201,7.45381934 15.8639176,7.45381934 L8.1360824,7.45381934 C5.64895285,7.45381934 3.63255855,9.47460595 3.63255855,11.9617373 C3.63255855,14.448858 5.64895285,16.4585488 8.1360824,16.4585488 L15.8639176,16.4585488 L15.8639176,16.4585488 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 882 B |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,238 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import * as oracleDBTypes from 'oracledb';
|
||||
|
||||
import type { ExecuteOpBindParam } from '../helpers/interfaces';
|
||||
import {
|
||||
addSortRules,
|
||||
getBindParameters,
|
||||
getCompatibleValue,
|
||||
getOutBindDefsForExecute,
|
||||
} from '../helpers/utils';
|
||||
|
||||
describe('Test addSortRules', () => {
|
||||
it('should ORDER BY ASC', () => {
|
||||
const query = 'SELECT * FROM "scott"."employees"';
|
||||
const sortRules = [{ column: 'id', direction: 'ASC' }];
|
||||
|
||||
const updatedQuery = addSortRules(query, sortRules);
|
||||
|
||||
expect(updatedQuery).toEqual(`${query} ORDER BY "id" ASC`);
|
||||
});
|
||||
|
||||
it('should ORDER BY DESC', () => {
|
||||
const query = 'SELECT * FROM "scott"."employees"';
|
||||
const sortRules = [{ column: 'id', direction: 'DESC' }];
|
||||
|
||||
const updatedQuery = addSortRules(query, sortRules);
|
||||
|
||||
expect(updatedQuery).toEqual(`${query} ORDER BY "id" DESC`);
|
||||
});
|
||||
|
||||
it('should handle multiple sort rules', () => {
|
||||
const query = 'SELECT * FROM "scott"."employees"';
|
||||
const sortRules = [
|
||||
{ column: 'id', direction: 'ASC' },
|
||||
{ column: 'name', direction: 'DESC' },
|
||||
];
|
||||
|
||||
const updatedQuery = addSortRules(query, sortRules);
|
||||
|
||||
expect(updatedQuery).toEqual(`${query} ORDER BY "id" ASC, "name" DESC`);
|
||||
});
|
||||
|
||||
it('should ignore incorrect direction', () => {
|
||||
const query = 'SELECT * FROM "scott"."employees"';
|
||||
const sortRules = [{ column: 'id', direction: 'SELECT * ' }];
|
||||
|
||||
const updatedQuery = addSortRules(query, sortRules);
|
||||
|
||||
expect(updatedQuery).toEqual(`${query} ORDER BY "id" ASC`); // by default we just use ASC
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test returning Clause', () => {
|
||||
it('should add RETURNING clause', () => {
|
||||
const query =
|
||||
'INSERT INTO "VECTOR"."FRUITS" ("FRUIT_ID","PRICE_PER_KG","FRUIT_NAME") VALUES (:0,:1,:2)';
|
||||
const metaData = {
|
||||
COLOR: {
|
||||
type: 'VARCHAR2',
|
||||
nullable: true,
|
||||
maxSize: 256,
|
||||
},
|
||||
FRUIT_ID: {
|
||||
type: 'NUMBER',
|
||||
nullable: false,
|
||||
maxSize: 22,
|
||||
},
|
||||
FRUIT_NAME: {
|
||||
type: 'VARCHAR2',
|
||||
nullable: false,
|
||||
maxSize: 256,
|
||||
},
|
||||
PRICE_PER_KG: {
|
||||
type: 'NUMBER',
|
||||
nullable: true,
|
||||
maxSize: 22,
|
||||
},
|
||||
};
|
||||
const outputColumns = ['FRUIT_NAME'];
|
||||
const bindInfo = [
|
||||
{
|
||||
type: oracleDBTypes.NUMBER,
|
||||
},
|
||||
{
|
||||
type: oracleDBTypes.NUMBER,
|
||||
},
|
||||
{
|
||||
type: oracleDBTypes.STRING,
|
||||
maxSize: 10000000,
|
||||
},
|
||||
];
|
||||
const bindIndex = 3;
|
||||
const expectedQuery = `${query} RETURNING "FRUIT_NAME" INTO :3`;
|
||||
|
||||
const updatedQuery = getOutBindDefsForExecute(
|
||||
query,
|
||||
metaData,
|
||||
outputColumns,
|
||||
bindInfo,
|
||||
bindIndex,
|
||||
);
|
||||
|
||||
expect(updatedQuery).toEqual(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getCompatibleValue ', () => {
|
||||
it('Verify DateTime object is accepted', () => {
|
||||
const jsDate = new Date(2024, 0, 1, 14, 30, 0);
|
||||
const dtUTC = DateTime.fromJSDate(jsDate, { zone: 'utc' });
|
||||
const result = getCompatibleValue('DATE', dtUTC);
|
||||
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getBindParameters ', () => {
|
||||
it('Verify different types are accepted', () => {
|
||||
const query = `INSERT INTO demo_all_types (id, col_number, col_varchar, col_char, col_date, col_timestamp, col_blob, col_json, col_bool, col_vector)
|
||||
VALUES (:pid, 12345.67, 'Hello World', 'ABC', DATE '2024-05-01', TIMESTAMP '2024-05-01 10:15:30', :pblob, :pjs, TRUE, :pvecsp)`;
|
||||
const paramList: ExecuteOpBindParam[] = [
|
||||
{
|
||||
name: 'pblob',
|
||||
bindDirection: 'in',
|
||||
datatype: 'blob',
|
||||
valueBlob: Buffer.from([
|
||||
98, 105, 110, 97, 114, 121, 95, 100, 97, 116, 97, 95, 104, 101, 114, 101, 32, 102, 111,
|
||||
114, 32, 66, 76, 79, 66,
|
||||
]),
|
||||
parseInStatement: false,
|
||||
},
|
||||
{
|
||||
name: 'pjs',
|
||||
bindDirection: 'in',
|
||||
datatype: 'json',
|
||||
valueJson: {
|
||||
user: 'John',
|
||||
active: true,
|
||||
roles: ['admin', 'developer'],
|
||||
},
|
||||
parseInStatement: false,
|
||||
},
|
||||
{
|
||||
name: 'pvecsp',
|
||||
bindDirection: 'in',
|
||||
datatype: 'sparse',
|
||||
valueSparse: {
|
||||
dimensions: 4,
|
||||
indices: [0, 2],
|
||||
values: [3, 4],
|
||||
},
|
||||
parseInStatement: false,
|
||||
},
|
||||
{
|
||||
name: 'pid',
|
||||
bindDirection: 'in',
|
||||
datatype: 'number',
|
||||
valueNumber: 2471,
|
||||
parseInStatement: false,
|
||||
},
|
||||
];
|
||||
|
||||
const expectedBindParams: any = {
|
||||
pblob: {
|
||||
type: oracleDBTypes.BLOB,
|
||||
val: Buffer.from([
|
||||
98, 105, 110, 97, 114, 121, 95, 100, 97, 116, 97, 95, 104, 101, 114, 101, 32, 102, 111,
|
||||
114, 32, 66, 76, 79, 66,
|
||||
]),
|
||||
dir: 3001,
|
||||
},
|
||||
pjs: {
|
||||
type: oracleDBTypes.DB_TYPE_JSON,
|
||||
val: {
|
||||
user: 'John',
|
||||
active: true,
|
||||
roles: ['admin', 'developer'],
|
||||
},
|
||||
dir: 3001,
|
||||
},
|
||||
pvecsp: {
|
||||
type: oracleDBTypes.DB_TYPE_VECTOR,
|
||||
val: new oracleDBTypes.SparseVector({
|
||||
indices: new Uint32Array([0, 2]),
|
||||
values: new Float64Array([3, 4]),
|
||||
numDimensions: 4,
|
||||
}),
|
||||
dir: 3001,
|
||||
},
|
||||
pid: {
|
||||
type: oracleDBTypes.NUMBER,
|
||||
val: 2471,
|
||||
dir: 3001,
|
||||
},
|
||||
};
|
||||
let updatedQuery: string;
|
||||
let bindParameters: oracleDBTypes.BindParameters;
|
||||
|
||||
// test Sparse Vector
|
||||
({ updatedQuery, bindParameters } = getBindParameters(query, paramList));
|
||||
expect(updatedQuery).toEqual(query);
|
||||
expect(bindParameters).toEqual(expectedBindParams);
|
||||
|
||||
// test VECTOR type
|
||||
paramList[2] = {
|
||||
name: 'pvecsp',
|
||||
bindDirection: 'in',
|
||||
datatype: 'vector',
|
||||
valueVector: [3, 4, 5, 6, 78],
|
||||
parseInStatement: false,
|
||||
};
|
||||
expectedBindParams.pvecsp = {
|
||||
type: oracleDBTypes.DB_TYPE_VECTOR,
|
||||
val: [3, 4, 5, 6, 78],
|
||||
dir: 3001,
|
||||
};
|
||||
({ updatedQuery, bindParameters } = getBindParameters(query, paramList));
|
||||
expect(updatedQuery).toEqual(query);
|
||||
expect(bindParameters).toEqual(expectedBindParams);
|
||||
|
||||
// test null value
|
||||
paramList[2] = {
|
||||
name: 'pvecsp',
|
||||
bindDirection: 'in',
|
||||
datatype: 'vector',
|
||||
valueVector: null,
|
||||
parseInStatement: false,
|
||||
};
|
||||
expectedBindParams.pvecsp = {
|
||||
type: oracleDBTypes.DB_TYPE_VECTOR,
|
||||
val: null,
|
||||
dir: 3001,
|
||||
};
|
||||
({ updatedQuery, bindParameters } = getBindParameters(query, paramList));
|
||||
expect(updatedQuery).toEqual(query);
|
||||
expect(bindParameters).toEqual(expectedBindParams);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ICredentialTestFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
ITriggerFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import oracledb from 'oracledb';
|
||||
|
||||
import { ConnectionPoolManager } from '@utils/connection-pool-manager';
|
||||
|
||||
import type { OracleDBNodeOptions, OracleDBNodeCredentials } from '../helpers/interfaces';
|
||||
|
||||
// used for thick mode to call initOracleClient API only once.
|
||||
let initializeDriverMode = false;
|
||||
|
||||
const getOracleDBConfig = (credentials: OracleDBNodeCredentials) => {
|
||||
const { useThickMode, useSSL, ...dbConfig } = {
|
||||
...credentials,
|
||||
privilege: credentials.privilege || undefined,
|
||||
};
|
||||
|
||||
return dbConfig;
|
||||
};
|
||||
|
||||
export async function configureOracleDB(
|
||||
this: IExecuteFunctions | ICredentialTestFunctions | ILoadOptionsFunctions | ITriggerFunctions,
|
||||
credentials: OracleDBNodeCredentials,
|
||||
options: OracleDBNodeOptions = {},
|
||||
): Promise<oracledb.Pool> {
|
||||
const poolManager = ConnectionPoolManager.getInstance(this.logger);
|
||||
const fallBackHandler = async (abortController: AbortController): Promise<oracledb.Pool> => {
|
||||
const dbConfig = getOracleDBConfig(credentials);
|
||||
|
||||
if (credentials.useThickMode) {
|
||||
if (!initializeDriverMode) {
|
||||
oracledb.initOracleClient();
|
||||
initializeDriverMode = true;
|
||||
}
|
||||
} else if (initializeDriverMode) {
|
||||
// Thick mode is initialized, cannot switch back to thin mode
|
||||
throw new Error('Thin mode can not be used after thick mode initialization');
|
||||
}
|
||||
const pool = await oracledb.createPool(dbConfig);
|
||||
|
||||
abortController.signal.addEventListener('abort', async () => {
|
||||
try {
|
||||
await pool.close();
|
||||
this.logger.debug('pool closed on abort');
|
||||
} catch (error) {
|
||||
this.logger.error('Error closing pool on abort', { error });
|
||||
}
|
||||
});
|
||||
return pool;
|
||||
};
|
||||
|
||||
return await poolManager.getConnection<oracledb.Pool>({
|
||||
credentials,
|
||||
nodeType: 'oracledb',
|
||||
nodeVersion: String(options.nodeVersion ?? '1'),
|
||||
fallBackHandler,
|
||||
wasUsed: (pool) => {
|
||||
if (pool) {
|
||||
this.logger.debug(`DB pool reused, open connections: ${pool.connectionsOpen}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user