From c87382c0867a2628b111d18291c9110d228feab2 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 10 Dec 2020 04:17:16 -0500 Subject: [PATCH] :sparkles: Add Snowflake Node (#1230) --- .../credentials/Snowflake.credentials.ts | 70 +++++ .../nodes/Snowflake/GenericFunctions.ts | 62 +++++ .../nodes/Snowflake/Snowflake.node.ts | 244 ++++++++++++++++++ .../nodes-base/nodes/Snowflake/snowflake.png | Bin 0 -> 2347 bytes packages/nodes-base/package.json | 4 + 5 files changed, 380 insertions(+) create mode 100644 packages/nodes-base/credentials/Snowflake.credentials.ts create mode 100644 packages/nodes-base/nodes/Snowflake/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Snowflake/Snowflake.node.ts create mode 100644 packages/nodes-base/nodes/Snowflake/snowflake.png diff --git a/packages/nodes-base/credentials/Snowflake.credentials.ts b/packages/nodes-base/credentials/Snowflake.credentials.ts new file mode 100644 index 0000000000..dde6b8df14 --- /dev/null +++ b/packages/nodes-base/credentials/Snowflake.credentials.ts @@ -0,0 +1,70 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class Snowflake implements ICredentialType { + name = 'snowflake'; + displayName = 'Snowflake'; + documentationUrl = 'snowflake'; + properties = [ + { + displayName: 'Account', + name: 'account', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Enter the name of your Snowflake account.', + }, + { + displayName: 'Database', + name: 'database', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Specify the database you want to use after creating the connection.', + }, + { + displayName: 'Warehouse', + name: 'warehouse', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The default virtual warehouse to use for the session after connecting. Used for performing queries, loading data, etc.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'Schema', + name: 'schema', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Enter the schema you want to use after creating the connection', + }, + { + displayName: 'Role', + name: 'role', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Enter the security role you want to use after creating the connection', + }, + { + displayName: 'Client Session Keep Alive', + name: 'clientSessionKeepAlive', + type: 'boolean' as NodePropertyTypes, + default: false, + description: `By default, client connections typically time out approximately 3-4 hours after the most recent query was executed.
+ If the parameter clientSessionKeepAlive is set to true, the client’s connection to the server will be kept alive indefinitely, even if no queries are executed.`, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts b/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts new file mode 100644 index 0000000000..193a43e1e1 --- /dev/null +++ b/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts @@ -0,0 +1,62 @@ +import { + IDataObject, + INodeExecutionData, +} from 'n8n-workflow'; + +import * as snowflake from 'snowflake-sdk'; + +export function connect(conn: snowflake.Connection) { + return new Promise((resolve, reject) => { + conn.connect((err, conn) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function destroy(conn: snowflake.Connection) { + return new Promise((resolve, reject) => { + conn.destroy((err, conn) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function execute(conn: snowflake.Connection, sqlText: string, binds: snowflake.InsertBinds) { + return new Promise((resolve, reject) => { + conn.execute({ + sqlText, + binds, + complete: (err, stmt, rows) => { + if (!err) { + resolve(rows); + } else { + reject(err); + } + }, + }); + }); +} + +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; + }); +} diff --git a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts new file mode 100644 index 0000000000..3a7cfeb260 --- /dev/null +++ b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts @@ -0,0 +1,244 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + connect, + copyInputItems, + destroy, + execute, +} from './GenericFunctions'; + +import * as snowflake from 'snowflake-sdk'; + +export class Snowflake implements INodeType { + description: INodeTypeDescription = { + displayName: 'Snowflake', + name: 'snowflake', + icon: 'file:snowflake.png', + group: ['input'], + version: 1, + description: 'Get, add and update data in Snowflake.', + defaults: { + name: 'Snowflake', + color: '#5ebbeb', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'snowflake', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Execute Query', + value: 'executeQuery', + description: 'Execute an SQL query.', + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert rows in database.', + }, + { + name: 'Update', + value: 'update', + description: 'Update 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('snowflake') as unknown as snowflake.ConnectionOptions; + const returnData: IDataObject[] = []; + let responseData; + + const connection = snowflake.createConnection(credentials); + + await connect(connection); + + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0) as string; + + if (operation === 'executeQuery') { + // ---------------------------------- + // executeQuery + // ---------------------------------- + + for (let i = 0; i < items.length; i++) { + const query = this.getNodeParameter('query', i) as string; + responseData = await execute(connection, query, []); + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + + 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 query = `INSERT INTO ${table}(${columns.join(',')}) VALUES (${columns.map(column => '?').join(',')})`; + const data = copyInputItems(items, columns); + const binds = data.map((element => Object.values(element))); + await execute(connection, query, binds as unknown as snowflake.InsertBinds); + returnData.push.apply(returnData, data); + } + + 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 query = `UPDATE ${table} SET ${columns.map(column => `${column} = ?`).join(',')} WHERE ${updateKey} = ?;`; + const data = copyInputItems(items, columns); + const binds = data.map((element => Object.values(element).concat(element[updateKey]))); + for (let i = 0; i < binds.length; i++) { + await execute(connection, query, binds[i] as unknown as snowflake.InsertBinds); + } + returnData.push.apply(returnData, data); + } + + await destroy(connection); + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Snowflake/snowflake.png b/packages/nodes-base/nodes/Snowflake/snowflake.png new file mode 100644 index 0000000000000000000000000000000000000000..6d149707ddc9eb560b698e4e9502db63b0deba02 GIT binary patch literal 2347 zcmX|Bc|6ox8$V{w8FMAMC6&~@3D;IPEiPr>2HD9dvb5huN((9#CF?bnRFblH-DIiI z#Gn~724S+3aw(LxEW?=PH+Q^$yr0i=p7T7d(;+HIH+U5Y@7F-=YZKNt7}LS|2eXRkhuU~ zPk=~iD6v!~RDOr$qMwl02@V+$w!<;So|5%X9%=O zM4v@3hhf<`to)8>Q@}S2=o$i_Ghm$thg`N{3cyni_Ezy&S(Gl;>%=Wz*)IXrC~r7>X9zqsN7NSlI1{qW5sF{cU9 zzvJR)`0z)vuSQVOCwy22KKvofZO329Bs~8}ET6zf=HhbO_SMh93-^E}8CN_AA2;KC zLQ%zc5O5JCmJ_m@fyOqN(F|OUgDrmmIuE#q0b4g|l~kpG*+SGHQvD=BTrp$|P%R%Z zXAnn-nwM%N%UnQZBd}r$KJ9er!XRUwKpP{}OvAFTFuk6@1b`3?ZEZ^V-op1}gd&C6(g?F5ydu@s5G` zs#*Nm+jz&rFtrwvFTl`Lc%u-oCD7Cj{@o264nx`)%iL|L;y+olN+ubN)I66q50GK)N?(6|D z8L1^5j;0?3Ga8y>@2Xw`yysZ(6IoY88fw zCnvuRe&}g$Z|63%Y8fvIo;}UYOuG{u9d$YMe8}ljL6npJem>rAF3t|N=ElamcN^{6 zv2BZ*s>-^x3d@9i-t6?$(5H_d-g8-RYbxK+Xr+bEpXEzKWMte3ryXaq%cEnxn<5zK zeN$i%oUVE5WMBEI|C6qLUrqS8{;F&KX+bWMYF>JamGk^W#GY5=pG3DL)2{>PoD06L z1{=Byx`L?s*>Tv$tz>o{?a%_rgBhBX%qCi0Tfw<_*763;m?U4Tf2>Sb#c*rrQMT@H=lbWfeqKh!+0ff4Y?IUbtNTc8o(dW( zvYy#9j!ypRX)NCw*QLaO7i}!obHAx_Q%<=bU=XjE$W0A7S@mI;6r#O08>A8&Iuu7+apgcq<3HL|g>&?( zcM)>bdIWL|HLtpMs@C$pU@ZEpgF3gf>QzbVNr@hLIwx*E5boHYJ-b|P>*}Nn4Z1B> z{;d&{HOC(P8vEy$391Hu|6NB?>b`~})Vk0LW(qMSfBWUUzdSyxpE-Mrea?_BQ!MQns)4aFn~z2C(-4&T;SPkEo5qYMnQXePkFDNt-8KqSy7BtbBC;R^Lb? zafV;)wrKO{S9Hrw5KkW zJy?5ylS1`db?^D+L^H*}W8=qssXFacSP8RTy}LzdAa7X;FM+px0+m{+|sn| zLAa+|Rg5hQcX-b=_x89HIGk1Kx45Fr^roTKrdogb)hBmZvh2JZ-Gdv6_8=?R*@ zGVb-dFh};s$k~dYC|ZhUq`P*skYueVB`@7fGOlak=JcK@v5t8~#vJ@Gw)VFy>!iN| z=b!Y}<{P;c$H%qH&abO3?a_sLNpFLKkZ~ZB!|910)1msm_yX7wbk=72ubTV9cE#_KiD73ix_frl5pVCL zxT?`pJme|DL+cZbEV?YVFt9V3H(f1FyQ6(|M(l#Fnp>>v-%wm?(0YGO1GA*cxqD`f za$nZ)#!!{_jc>AIuJ89w{>6uA8F-Cx^kckop*?8Dze$f!m{H2s}Q>61`g>Fo6OXf1Wkl!d@ zG#`FS-!#sDN_@BDW%=D0CDBn+qGf@7h{NgUx+f2Ndm6X~%k(Jr>!y~Q@N%x3b>g86 zZ*M{r$Shn|HJuHJ%^l?w3zLL{f5^xv%8YtbC@OQCe(BQUpK}PeX+CWK7N11VemngMd*!!4AU5iOja;Ymud9bFvj@h%|q$e5A1E6t&1!JuKx#6UvftP literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 9f1ccd681e..f22717a75b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -183,6 +183,7 @@ "dist/credentials/SlackApi.credentials.js", "dist/credentials/SlackOAuth2Api.credentials.js", "dist/credentials/Sms77Api.credentials.js", + "dist/credentials/Snowflake.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/StravaOAuth2Api.credentials.js", @@ -410,6 +411,7 @@ "dist/nodes/Signl4/Signl4.node.js", "dist/nodes/Slack/Slack.node.js", "dist/nodes/Sms77/Sms77.node.js", + "dist/nodes/Snowflake/Snowflake.node.js", "dist/nodes/SplitInBatches.node.js", "dist/nodes/Spontit/Spontit.node.js", "dist/nodes/Spotify/Spotify.node.js", @@ -497,6 +499,7 @@ }, "dependencies": { "@types/promise-ftp": "^1.3.4", + "@types/snowflake-sdk": "^1.5.1", "aws4": "^1.8.0", "basic-auth": "^2.0.1", "change-case": "^4.1.1", @@ -531,6 +534,7 @@ "request": "^2.88.2", "rhea": "^1.0.11", "rss-parser": "^3.7.0", + "snowflake-sdk": "^1.5.3", "ssh2-sftp-client": "^5.2.1", "tmp-promise": "^3.0.2", "uuid": "^3.4.0",