n8n/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts
Iván Ovejero beedfb609c
feat(editor): SQL editor overhaul (#6282)
* Draft setup
*  Implemented expression evaluation in Postgres node, minor SQL editor UI improvements, minor refacring
*  Added initial version of expression preview for SQL editor
*  Linking npm package for codemirror sql grammar instead of a local file
*  Moving expression editor wrapper elements to the component
*  Using expression preview in SQL editor
* Use SQL parser skipping whitespace
*  Added support for custom skipped segments specification
*  Fixing highlight problems with dots and expressions that resolve to zero
* 👕 Fixing linting error
*  Added current item support
*  Added expression support to more nodes with sql editor
*  Added expression support for other nodes
*  Implemented different SQL dialect support
* 🐛 Fixing hard-coded parameter names for editors
*  Fixing preview for nested queries, updating query when input data changes, adding keyboard shortcut to toggle comments
*  Adding a custom automcomplete notice for different editors
*  Updating SQL autocomplete notice
*  Added unit tests for SQL editor
*  Using latest grammar
* 🐛 Fixing code node editor rendering
* 💄 SQL preview dropdown matches editor width. Removing unnecessary css
*  Addressing PR review feedback
* 👌 Addressing PR review feedback pt2
* 👌 Added path alias for utils in nodes-base package
* 👌 Addressing more PR review feedback
*  Adding tests for `getResolvables` utility function
* Fixing lodash imports
* 👌 Better focus handling, adding more plugins to the editor, other minor imrovements
*  Not showing SQL autocomplete suggestions inside expressions
*  Using npm package for sql grammar
*  Removing autocomplete notice, adding line highlight on syntax error
* 👌 Addressing code review feedback
---------
Co-authored-by: Milorad Filipovic <milorad@n8n.io>
2023-06-22 16:47:28 +02:00

454 lines
12 KiB
TypeScript

import type {
IExecuteFunctions,
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { chunk, flatten, getResolvables } from '@utils/utilities';
import mssql from 'mssql';
import type { ITables } from './TableInterface';
import {
copyInputItem,
createTableStruct,
executeQueryQueue,
extractDeleteValues,
extractUpdateCondition,
extractUpdateSet,
extractValues,
formatColumns,
} from './GenericFunctions';
export class MicrosoftSql implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft SQL',
name: 'microsoftSql',
icon: 'file:mssql.svg',
group: ['input'],
version: 1,
description: 'Get, add and update data in Microsoft SQL',
defaults: {
name: 'Microsoft SQL',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'microsoftSql',
required: true,
testedBy: 'microsoftSqlConnectionTest',
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Execute an SQL query',
action: 'Execute a SQL query',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database',
action: 'Insert rows in database',
},
{
name: 'Update',
value: 'update',
description: 'Update rows in database',
action: 'Update rows in database',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete rows in database',
action: 'Delete rows in database',
},
],
default: 'insert',
},
// ----------------------------------
// executeQuery
// ----------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'MSSQL',
},
displayOptions: {
show: {
operation: ['executeQuery'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
placeholder: 'SELECT id, name FROM product WHERE id < 40',
required: true,
description: 'The SQL query to execute',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
required: true,
description: 'Name of the table in which to insert data to',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
placeholder: 'id,name,description',
description:
'Comma-separated list of the properties which should used as columns for the new rows',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
required: true,
description: 'Name of the table in which to update data in',
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: 'id',
required: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id
description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
placeholder: 'name,description',
description:
'Comma-separated list of the properties which should used as columns for rows to update',
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: '',
required: true,
description: 'Name of the table in which to delete data',
},
{
displayName: 'Delete Key',
name: 'deleteKey',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: 'id',
required: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id
description:
'Name of the property which decides which rows in the database should be deleted. Normally that would be "id".',
},
],
};
methods = {
credentialTest: {
async microsoftSqlConnectionTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data as ICredentialDataDecryptedObject;
try {
const config = {
server: credentials.server as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
domain: credentials.domain ? (credentials.domain as string) : undefined,
connectionTimeout: credentials.connectTimeout as number,
requestTimeout: credentials.requestTimeout as number,
options: {
encrypt: credentials.tls as boolean,
enableArithAbort: false,
tdsVersion: credentials.tdsVersion as string,
trustServerCertificate: credentials.allowUnauthorizedCerts as boolean,
},
};
const pool = new mssql.ConnectionPool(config);
await pool.connect();
} catch (error) {
return {
status: 'Error',
message: error.message,
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = await this.getCredentials('microsoftSql');
const config = {
server: credentials.server as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
domain: credentials.domain ? (credentials.domain as string) : undefined,
connectionTimeout: credentials.connectTimeout as number,
requestTimeout: credentials.requestTimeout as number,
options: {
encrypt: credentials.tls as boolean,
enableArithAbort: false,
tdsVersion: credentials.tdsVersion as string,
trustServerCertificate: credentials.allowUnauthorizedCerts as boolean,
},
};
const pool = new mssql.ConnectionPool(config);
await pool.connect();
const returnItems: INodeExecutionData[] = [];
let responseData: IDataObject | IDataObject[] = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0);
try {
if (operation === 'executeQuery') {
// ----------------------------------
// executeQuery
// ----------------------------------
let rawQuery = this.getNodeParameter('query', 0) as string;
for (const resolvable of getResolvables(rawQuery)) {
rawQuery = rawQuery.replace(resolvable, this.evaluateExpression(resolvable, 0) as string);
}
const queryResult = await pool.request().query(rawQuery);
const result =
queryResult.recordsets.length > 1
? flatten(queryResult.recordsets)
: queryResult.recordsets[0];
responseData = result;
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const tables = createTableStruct(this.getNodeParameter, items);
await executeQueryQueue(
tables,
({
table,
columnString,
// eslint-disable-next-line @typescript-eslint/no-shadow
items,
}: {
table: string;
columnString: string;
items: IDataObject[];
}): Array<Promise<object>> => {
return chunk(items, 1000).map(async (insertValues) => {
const values = insertValues.map((item: IDataObject) => extractValues(item)).join(',');
return pool
.request()
.query(`INSERT INTO ${table}(${formatColumns(columnString)}) VALUES ${values};`);
});
},
);
responseData = items;
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const updateKeys = items.map(
(item, index) => this.getNodeParameter('updateKey', index) as string,
);
const tables = createTableStruct(
this.getNodeParameter,
items,
['updateKey'].concat(updateKeys),
'updateKey',
);
await executeQueryQueue(
tables,
({
table,
columnString,
// eslint-disable-next-line @typescript-eslint/no-shadow
items,
}: {
table: string;
columnString: string;
items: IDataObject[];
}): Array<Promise<object>> => {
return items.map(async (item) => {
const columns = columnString.split(',').map((column) => column.trim());
const setValues = extractUpdateSet(item, columns);
const condition = extractUpdateCondition(item, item.updateKey as string);
return pool.request().query(`UPDATE ${table} SET ${setValues} WHERE ${condition};`);
});
},
);
responseData = items;
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const tables = items.reduce((acc, item, index) => {
const table = this.getNodeParameter('table', index) as string;
const deleteKey = this.getNodeParameter('deleteKey', index) as string;
if (acc[table] === undefined) {
acc[table] = {};
}
if (acc[table][deleteKey] === undefined) {
acc[table][deleteKey] = [];
}
acc[table][deleteKey].push(item);
return acc;
}, {} as ITables);
const queriesResults = await Promise.all(
Object.keys(tables).map(async (table) => {
const deleteKeyResults = Object.keys(tables[table]).map(async (deleteKey) => {
const deleteItemsList = chunk(
tables[table][deleteKey].map((item) =>
copyInputItem(item as INodeExecutionData, [deleteKey]),
),
1000,
);
const queryQueue = deleteItemsList.map(async (deleteValues) => {
return pool
.request()
.query(
`DELETE FROM ${table} WHERE "${deleteKey}" IN ${extractDeleteValues(
deleteValues as IDataObject[],
deleteKey,
)};`,
);
});
return Promise.all(queryQueue);
});
return Promise.all(deleteKeyResults);
}),
);
const rowsDeleted = flatten(queriesResults).reduce(
(acc: number, resp: mssql.IResult<object>): number =>
(acc += resp.rowsAffected.reduce((sum, val) => (sum += val))),
0,
);
responseData = rowsDeleted;
} else {
await pool.close();
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported!`,
);
}
} catch (error) {
if (this.continueOnFail()) {
responseData = items;
} else {
await pool.close();
throw error;
}
}
// Close the connection
await pool.close();
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData: { item: 0 } },
);
returnItems.push(...executionData);
return this.prepareOutputData(returnItems);
}
}