From 040b0a690ce91d12609b65caa02a53ea35cd6a81 Mon Sep 17 00:00:00 2001 From: Eric Chang Date: Thu, 14 Nov 2019 11:32:26 +0800 Subject: [PATCH 1/3] feat(credential): add credential of mysql node --- .../credentials/MySQL.credentials.ts | 45 +++++++++++++++++++ packages/nodes-base/package.json | 1 + 2 files changed, 46 insertions(+) create mode 100644 packages/nodes-base/credentials/MySQL.credentials.ts diff --git a/packages/nodes-base/credentials/MySQL.credentials.ts b/packages/nodes-base/credentials/MySQL.credentials.ts new file mode 100644 index 0000000000..de4335a274 --- /dev/null +++ b/packages/nodes-base/credentials/MySQL.credentials.ts @@ -0,0 +1,45 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class MySQL implements ICredentialType { + name = 'mysql'; + displayName = 'MySQL'; + properties = [ + { + displayName: 'Host', + name: 'host', + type: 'string' as NodePropertyTypes, + default: 'localhost', + }, + { + displayName: 'Database', + name: 'database', + type: 'string' as NodePropertyTypes, + default: 'mysql', + }, + { + displayName: 'User', + name: 'user', + type: 'string' as NodePropertyTypes, + default: 'mysql', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'Port', + name: 'port', + type: 'number' as NodePropertyTypes, + default: 3306, + }, + ]; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 75e190f244..d789a166df 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -52,6 +52,7 @@ "dist/credentials/MandrillApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", "dist/credentials/MongoDb.credentials.js", + "dist/credentials/MySQL.credentials.js", "dist/credentials/NextCloudApi.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/PipedriveApi.credentials.js", From c34f4841c28b021204fa19f988e20126d307f721 Mon Sep 17 00:00:00 2001 From: Eric Chang Date: Tue, 19 Nov 2019 16:36:56 +0800 Subject: [PATCH 2/3] feat(node): add mysql node --- .../nodes/MySQL/GenericFunctions.ts | 28 ++ packages/nodes-base/nodes/MySQL/MySQL.node.ts | 250 ++++++++++++++++++ packages/nodes-base/nodes/MySQL/mysql.png | Bin 0 -> 3546 bytes packages/nodes-base/package.json | 3 + 4 files changed, 281 insertions(+) create mode 100644 packages/nodes-base/nodes/MySQL/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/MySQL/MySQL.node.ts create mode 100644 packages/nodes-base/nodes/MySQL/mysql.png diff --git a/packages/nodes-base/nodes/MySQL/GenericFunctions.ts b/packages/nodes-base/nodes/MySQL/GenericFunctions.ts new file mode 100644 index 0000000000..2c6c3b01a6 --- /dev/null +++ b/packages/nodes-base/nodes/MySQL/GenericFunctions.ts @@ -0,0 +1,28 @@ +import { + IDataObject, + INodeExecutionData, +} from 'n8n-workflow'; + +/** + * Returns of copy of the items which only contains the json data and + * of that only the define properties + * + * @param {INodeExecutionData[]} items The items to copy + * @param {string[]} properties The properties it should include + * @returns + */ +export function copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[] { + // Prepare the data to insert and copy it to be returned + let newItem: IDataObject; + return items.map((item) => { + newItem = {}; + for (const property of properties) { + if (item.json[property] === undefined) { + newItem[property] = null; + } else { + newItem[property] = JSON.parse(JSON.stringify(item.json[property])); + } + } + return newItem; + }); +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/MySQL/MySQL.node.ts b/packages/nodes-base/nodes/MySQL/MySQL.node.ts new file mode 100644 index 0000000000..46ff00f2b2 --- /dev/null +++ b/packages/nodes-base/nodes/MySQL/MySQL.node.ts @@ -0,0 +1,250 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import * as knex from 'knex'; + +import { copyInputItems } from './GenericFunctions'; + +export class MySQL implements INodeType { + description: INodeTypeDescription = { + displayName: 'MySQL', + name: 'mysql', + icon: 'file:mysql.png', + group: ['input'], + version: 1, + description: 'Gets, add and update data in MySQL.', + defaults: { + name: 'MySQL', + color: '#4279a2', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'mysql', + required: true, + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Execute Query', + value: 'executeQuery', + description: 'Executes a SQL query.', + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert rows in database.', + }, + { + name: 'Update', + value: 'update', + description: 'Updates rows in database.', + }, + ], + default: 'insert', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // executeQuery + // ---------------------------------- + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + rows: 5, + }, + 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: '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: '', + 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, + 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.', + }, + + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials('mysql'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const client = knex({ client: 'mysql2', connection: credentials }); + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0) as string; + let returnItems = []; + + if (operation === 'executeQuery') { + // ---------------------------------- + // executeQuery + // ---------------------------------- + + const queryQueue = items.map((item, index) => { + const rawQuery = this.getNodeParameter('query', index) as string; + + return client.raw(rawQuery); + }); + let queryResult = await Promise.all(queryQueue); + + queryResult = queryResult.reduce((result, current) => { + if (Array.isArray(current[0])) { + return result.concat(current[0]); + } + + result.push(current[0]); + + return result; + }, []); + + returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); + + } else if (operation === 'insert') { + // ---------------------------------- + // insert + // ---------------------------------- + + const table = this.getNodeParameter('table', 0) as string; + const columnString = this.getNodeParameter('columns', 0) as string; + const columns = columnString.split(',').map(column => column.trim()); + const insertItems = copyInputItems(items, columns); + const insertData = await client.insert(insertItems).into(table); + + returnItems = [{ json: { row_count: insertData[0] }}]; + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + const table = this.getNodeParameter('table', 0) 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 = copyInputItems(items, columns); + const queryQueue = updateItems.map((item) => { + return client(table) + .where(updateKey, '=', item[updateKey]) + .update(item); + }); + + await Promise.all(queryQueue); + + returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]); + + } else { + throw new Error(`The operation "${operation}" is not supported!`); + } + + return this.prepareOutputData(returnItems); + } +} diff --git a/packages/nodes-base/nodes/MySQL/mysql.png b/packages/nodes-base/nodes/MySQL/mysql.png new file mode 100644 index 0000000000000000000000000000000000000000..b5202c0f41fe8e194f42a77fb532b1b290fc6830 GIT binary patch literal 3546 zcmV<04JGo4P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 Date: Thu, 12 Dec 2019 23:57:59 +0800 Subject: [PATCH 3/3] feat(node): remove knex dependency from MySQL node --- packages/nodes-base/nodes/MySQL/MySQL.node.ts | 39 ++++++++++--------- packages/nodes-base/package.json | 1 - 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/nodes-base/nodes/MySQL/MySQL.node.ts b/packages/nodes-base/nodes/MySQL/MySQL.node.ts index 46ff00f2b2..c2d54e4c66 100644 --- a/packages/nodes-base/nodes/MySQL/MySQL.node.ts +++ b/packages/nodes-base/nodes/MySQL/MySQL.node.ts @@ -5,7 +5,8 @@ import { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import * as knex from 'knex'; +// @ts-ignore +import * as mysql2 from 'mysql2/promise'; import { copyInputItems } from './GenericFunctions'; @@ -174,7 +175,7 @@ export class MySQL implements INodeType { throw new Error('No credentials got returned!'); } - const client = knex({ client: 'mysql2', connection: credentials }); + const connection = await mysql2.createConnection(credentials); const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0) as string; let returnItems = []; @@ -187,18 +188,20 @@ export class MySQL implements INodeType { const queryQueue = items.map((item, index) => { const rawQuery = this.getNodeParameter('query', index) as string; - return client.raw(rawQuery); + return connection.query(rawQuery); }); let queryResult = await Promise.all(queryQueue); - queryResult = queryResult.reduce((result, current) => { - if (Array.isArray(current[0])) { - return result.concat(current[0]); + queryResult = queryResult.reduce((collection, result) => { + const [rows, fields] = result; + + if (Array.isArray(rows)) { + return collection.concat(rows); } - result.push(current[0]); + collection.push(rows); - return result; + return collection; }, []); returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); @@ -212,9 +215,12 @@ export class MySQL implements INodeType { const columnString = this.getNodeParameter('columns', 0) as string; const columns = columnString.split(',').map(column => column.trim()); const insertItems = copyInputItems(items, columns); - const insertData = await client.insert(insertItems).into(table); + const insertPlaceholder = `(${columns.map(column => '?').join(',')})`; + const insertSQL = `INSERT INTO ${table}(${columnString}) VALUES ${items.map(item => insertPlaceholder).join(',')};`; + const queryItems = insertItems.reduce((collection, item) => collection.concat(Object.values(item as any)), []); + const queryResult = await connection.query(insertSQL, queryItems); - returnItems = [{ json: { row_count: insertData[0] }}]; + returnItems = this.helpers.returnJsonArray(queryResult[0] as IDataObject); } else if (operation === 'update') { // ---------------------------------- @@ -231,15 +237,12 @@ export class MySQL implements INodeType { } const updateItems = copyInputItems(items, columns); - const queryQueue = updateItems.map((item) => { - return client(table) - .where(updateKey, '=', item[updateKey]) - .update(item); - }); + const updateSQL = `UPDATE ${table} SET ${columns.map(column => `${column} = ?`).join(',')} WHERE ${updateKey} = ?;`; + const queryQueue = updateItems.map((item) => connection.query(updateSQL, Object.values(item).concat(item[updateKey]))); + let queryResult = await Promise.all(queryQueue); - await Promise.all(queryQueue); - - returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]); + queryResult = queryResult.map(result => result[0]); + returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); } else { throw new Error(`The operation "${operation}" is not supported!`); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6eb11b19f0..d19b7b2e85 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -187,7 +187,6 @@ "gm": "^1.23.1", "googleapis": "^42.0.0", "imap-simple": "^4.3.0", - "knex": "^0.20.1", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "lodash.unset": "^4.5.2",