mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-27 13:39:44 -08:00
071e6d6b6e
## Summary <img width="1240" alt="image" src="https://github.com/n8n-io/n8n/assets/8850410/2819f4ce-c343-431a-8a88-a1bc9c4b572a"> <img width="2649" alt="image" src="https://github.com/n8n-io/n8n/assets/8850410/36862aaf-cc4c-4668-bdc8-cf5a6f00babe"> 1. Add code node and open it 3. Click the fullscreen button in the bottom right 4. A fullscreen dialog should appear and allow editing the code 5. Changes made in the fullscreen dialog should be applied to the original code editor when closed It should work the same way for HTML/SQL/JSON editors ⚠️ Modal layout was updated so that modals/dialogs are centered, try to test some modals ## Related tickets and issues https://linear.app/n8n/issue/NODE-1009/add-fullscreen-view-to-code-node ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --------- Co-authored-by: Giulio Andreini <andreini@netseven.it>
435 lines
11 KiB
TypeScript
435 lines
11 KiB
TypeScript
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
|
import type {
|
|
ICredentialDataDecryptedObject,
|
|
ICredentialsDecrypted,
|
|
ICredentialTestFunctions,
|
|
IDataObject,
|
|
IExecuteFunctions,
|
|
INodeCredentialTestResult,
|
|
INodeExecutionData,
|
|
INodeType,
|
|
INodeTypeBaseDescription,
|
|
INodeTypeDescription,
|
|
} from 'n8n-workflow';
|
|
import { NodeOperationError } from 'n8n-workflow';
|
|
|
|
import type mysql2 from 'mysql2/promise';
|
|
|
|
import { createConnection, searchTables } from './GenericFunctions';
|
|
|
|
import { oldVersionNotice } from '@utils/descriptions';
|
|
import { getResolvables } from '@utils/utilities';
|
|
|
|
const versionDescription: INodeTypeDescription = {
|
|
displayName: 'MySQL',
|
|
name: 'mySql',
|
|
icon: 'file:mysql.svg',
|
|
group: ['input'],
|
|
version: 1,
|
|
description: 'Get, add and update data in MySQL',
|
|
defaults: {
|
|
name: 'MySQL',
|
|
},
|
|
inputs: ['main'],
|
|
outputs: ['main'],
|
|
credentials: [
|
|
{
|
|
name: 'mySql',
|
|
required: true,
|
|
testedBy: 'mysqlConnectionTest',
|
|
},
|
|
],
|
|
properties: [
|
|
oldVersionNotice,
|
|
{
|
|
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',
|
|
},
|
|
],
|
|
default: 'insert',
|
|
},
|
|
|
|
// ----------------------------------
|
|
// executeQuery
|
|
// ----------------------------------
|
|
{
|
|
displayName: 'Query',
|
|
name: 'query',
|
|
type: 'string',
|
|
noDataExpression: true,
|
|
typeOptions: {
|
|
editor: 'sqlEditor',
|
|
sqlDialect: 'MySQL',
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['executeQuery'],
|
|
},
|
|
},
|
|
default: '',
|
|
placeholder: 'SELECT id, name FROM product WHERE id < 40',
|
|
required: true,
|
|
description: 'The SQL query to execute',
|
|
},
|
|
|
|
// ----------------------------------
|
|
// insert
|
|
// ----------------------------------
|
|
{
|
|
displayName: 'Table',
|
|
name: 'table',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
placeholder: 'Select a Table...',
|
|
typeOptions: {
|
|
searchListMethod: 'searchTables',
|
|
searchFilterRequired: false,
|
|
searchable: true,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Name',
|
|
name: 'name',
|
|
type: 'string',
|
|
placeholder: 'table_name',
|
|
},
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['insert'],
|
|
},
|
|
},
|
|
description: 'Name of the table in which to insert data to',
|
|
},
|
|
{
|
|
displayName: 'Columns',
|
|
name: 'columns',
|
|
type: 'string',
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['insert'],
|
|
},
|
|
},
|
|
requiresDataPath: 'multiple',
|
|
default: '',
|
|
placeholder: 'id,name,description',
|
|
description:
|
|
'Comma-separated list of the properties which should used as columns for the new rows',
|
|
},
|
|
{
|
|
displayName: 'Options',
|
|
name: 'options',
|
|
type: 'collection',
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['insert'],
|
|
},
|
|
},
|
|
default: {},
|
|
placeholder: 'Add modifiers',
|
|
description: 'Modifiers for INSERT statement',
|
|
options: [
|
|
{
|
|
displayName: 'Ignore',
|
|
name: 'ignore',
|
|
type: 'boolean',
|
|
default: true,
|
|
description:
|
|
'Whether to ignore any ignorable errors that occur while executing the INSERT statement',
|
|
},
|
|
{
|
|
displayName: 'Priority',
|
|
name: 'priority',
|
|
type: 'options',
|
|
options: [
|
|
{
|
|
name: 'Low Prioirity',
|
|
value: 'LOW_PRIORITY',
|
|
description:
|
|
'Delays execution of the INSERT until no other clients are reading from the table',
|
|
},
|
|
{
|
|
name: 'High Priority',
|
|
value: 'HIGH_PRIORITY',
|
|
description:
|
|
'Overrides the effect of the --low-priority-updates option if the server was started with that option. It also causes concurrent inserts not to be used.',
|
|
},
|
|
],
|
|
default: 'LOW_PRIORITY',
|
|
description:
|
|
'Ignore any ignorable errors that occur while executing the INSERT statement',
|
|
},
|
|
],
|
|
},
|
|
|
|
// ----------------------------------
|
|
// update
|
|
// ----------------------------------
|
|
{
|
|
displayName: 'Table',
|
|
name: 'table',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
placeholder: 'Select a Table...',
|
|
typeOptions: {
|
|
searchListMethod: 'searchTables',
|
|
searchFilterRequired: false,
|
|
searchable: true,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Name',
|
|
name: 'name',
|
|
type: 'string',
|
|
placeholder: 'table_name',
|
|
},
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['update'],
|
|
},
|
|
},
|
|
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',
|
|
requiresDataPath: 'multiple',
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['update'],
|
|
},
|
|
},
|
|
default: '',
|
|
placeholder: 'name,description',
|
|
description:
|
|
'Comma-separated list of the properties which should used as columns for rows to update',
|
|
},
|
|
],
|
|
};
|
|
|
|
export class MySqlV1 implements INodeType {
|
|
description: INodeTypeDescription;
|
|
|
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
|
this.description = {
|
|
...baseDescription,
|
|
...versionDescription,
|
|
};
|
|
}
|
|
|
|
methods = {
|
|
credentialTest: {
|
|
async mysqlConnectionTest(
|
|
this: ICredentialTestFunctions,
|
|
credential: ICredentialsDecrypted,
|
|
): Promise<INodeCredentialTestResult> {
|
|
const credentials = credential.data as ICredentialDataDecryptedObject;
|
|
try {
|
|
const connection = await createConnection(credentials);
|
|
await connection.end();
|
|
} catch (error) {
|
|
return {
|
|
status: 'Error',
|
|
message: error.message,
|
|
};
|
|
}
|
|
return {
|
|
status: 'OK',
|
|
message: 'Connection successful!',
|
|
};
|
|
},
|
|
},
|
|
listSearch: {
|
|
searchTables,
|
|
},
|
|
};
|
|
|
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
const credentials = await this.getCredentials('mySql');
|
|
const connection = await createConnection(credentials);
|
|
const items = this.getInputData();
|
|
const operation = this.getNodeParameter('operation', 0);
|
|
let returnItems: INodeExecutionData[] = [];
|
|
|
|
if (operation === 'executeQuery') {
|
|
// ----------------------------------
|
|
// executeQuery
|
|
// ----------------------------------
|
|
|
|
try {
|
|
const queryQueue = items.map(async (_, index) => {
|
|
let rawQuery = (this.getNodeParameter('query', index) as string).trim();
|
|
|
|
for (const resolvable of getResolvables(rawQuery)) {
|
|
rawQuery = rawQuery.replace(
|
|
resolvable,
|
|
this.evaluateExpression(resolvable, index) as string,
|
|
);
|
|
}
|
|
|
|
return connection.query(rawQuery);
|
|
});
|
|
|
|
returnItems = ((await Promise.all(queryQueue)) as mysql2.OkPacket[][]).reduce(
|
|
(collection, result, index) => {
|
|
const [rows] = result;
|
|
|
|
const executionData = this.helpers.constructExecutionMetaData(
|
|
this.helpers.returnJsonArray(rows as unknown as IDataObject[]),
|
|
{ itemData: { item: index } },
|
|
);
|
|
|
|
collection.push(...executionData);
|
|
|
|
return collection;
|
|
},
|
|
[] as INodeExecutionData[],
|
|
);
|
|
} catch (error) {
|
|
if (this.continueOnFail()) {
|
|
returnItems = this.helpers.returnJsonArray({ error: error.message });
|
|
} else {
|
|
await connection.end();
|
|
throw error;
|
|
}
|
|
}
|
|
} else if (operation === 'insert') {
|
|
// ----------------------------------
|
|
// insert
|
|
// ----------------------------------
|
|
|
|
try {
|
|
const table = this.getNodeParameter('table', 0, '', { extractValue: true }) as string;
|
|
const columnString = this.getNodeParameter('columns', 0) as string;
|
|
const columns = columnString.split(',').map((column) => column.trim());
|
|
const insertItems = this.helpers.copyInputItems(items, columns);
|
|
const insertPlaceholder = `(${columns.map((_column) => '?').join(',')})`;
|
|
const options = this.getNodeParameter('options', 0);
|
|
const insertIgnore = options.ignore as boolean;
|
|
const insertPriority = options.priority as string;
|
|
|
|
const insertSQL = `INSERT ${insertPriority || ''} ${
|
|
insertIgnore ? 'IGNORE' : ''
|
|
} INTO ${table}(${columnString}) VALUES ${items
|
|
.map((_item) => insertPlaceholder)
|
|
.join(',')};`;
|
|
const queryItems = insertItems.reduce(
|
|
(collection: IDataObject[], item) =>
|
|
collection.concat(Object.values(item) as IDataObject[]),
|
|
[],
|
|
);
|
|
|
|
const queryResult = await connection.query(insertSQL, queryItems);
|
|
|
|
returnItems = this.helpers.returnJsonArray(queryResult[0] as unknown as IDataObject);
|
|
} catch (error) {
|
|
if (this.continueOnFail()) {
|
|
returnItems = this.helpers.returnJsonArray({ error: error.message });
|
|
} else {
|
|
await connection.end();
|
|
throw error;
|
|
}
|
|
}
|
|
} else if (operation === 'update') {
|
|
// ----------------------------------
|
|
// update
|
|
// ----------------------------------
|
|
|
|
try {
|
|
const table = this.getNodeParameter('table', 0, '', { extractValue: true }) as string;
|
|
const updateKey = this.getNodeParameter('updateKey', 0) as string;
|
|
const columnString = this.getNodeParameter('columns', 0) as string;
|
|
const columns = columnString.split(',').map((column) => column.trim());
|
|
|
|
if (!columns.includes(updateKey)) {
|
|
columns.unshift(updateKey);
|
|
}
|
|
|
|
const updateItems = this.helpers.copyInputItems(items, columns);
|
|
const updateSQL = `UPDATE ${table} SET ${columns
|
|
.map((column) => `${column} = ?`)
|
|
.join(',')} WHERE ${updateKey} = ?;`;
|
|
const queryQueue = updateItems.map(async (item) =>
|
|
connection.query(updateSQL, Object.values(item).concat(item[updateKey])),
|
|
);
|
|
const queryResult = await Promise.all(queryQueue);
|
|
returnItems = this.helpers.returnJsonArray(
|
|
queryResult.map((result) => result[0]) as unknown as IDataObject[],
|
|
);
|
|
} catch (error) {
|
|
if (this.continueOnFail()) {
|
|
returnItems = this.helpers.returnJsonArray({ error: error.message });
|
|
} else {
|
|
await connection.end();
|
|
throw error;
|
|
}
|
|
}
|
|
} else {
|
|
if (this.continueOnFail()) {
|
|
returnItems = this.helpers.returnJsonArray({
|
|
error: `The operation "${operation}" is not supported!`,
|
|
});
|
|
} else {
|
|
await connection.end();
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
`The operation "${operation}" is not supported!`,
|
|
);
|
|
}
|
|
}
|
|
|
|
await connection.end();
|
|
|
|
return [returnItems];
|
|
}
|
|
}
|