From 8b04f1ff37ea824c6a6179776c20183dfebae341 Mon Sep 17 00:00:00 2001 From: Frane Bandov Date: Wed, 30 Oct 2019 12:05:52 +0100 Subject: [PATCH 1/3] GraphQL support --- .../nodes-base/nodes/GraphQL/GraphQL.node.ts | 228 ++++++++++++++++++ packages/nodes-base/package.json | 1 + 2 files changed, 229 insertions(+) create mode 100644 packages/nodes-base/nodes/GraphQL/GraphQL.node.ts diff --git a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts new file mode 100644 index 0000000000..8c5e438af1 --- /dev/null +++ b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts @@ -0,0 +1,228 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { OptionsWithUri } from 'request'; +import { RequestPromiseOptions } from 'request-promise-native'; + +export class GraphQL implements INodeType { + description: INodeTypeDescription = { + displayName: 'GraphQL', + name: 'graphql', + group: ['input'], + version: 1, + description: 'Makes a GraphQL request and returns the received data', + defaults: { + name: 'GraphQL', + color: '#E10098', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'HTTP Request Method', + name: 'requestMethod', + type: 'options', + options: [ + { + name: 'GET', + value: 'GET' + }, + { + name: 'POST', + value: 'POST' + }, + ], + default: 'POST', + description: 'The underlying HTTP request method to use.', + }, + { + displayName: 'Endpoint', + name: 'endpoint', + type: 'string', + default: '', + placeholder: 'http://example.com/graphql', + description: 'The GraphQL endpoint.', + required: true, + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + default: false, + description: 'Still fetch the response even if SSL certificate validation is not possible.', + }, + { + displayName: 'Request Format', + name: 'requestFormat', + type: 'options', + required: true, + options: [ + { + name: 'GraphQL (raw)', + value: 'graphql' + }, + { + name: 'JSON', + value: 'json' + }, + ], + displayOptions: { + show: { + requestMethod: [ + "POST" + ], + }, + }, + default: 'graphql', + description: 'The format for the query payload', + }, + { + displayName: 'Query', + name: 'query', + type: 'json', + default: '', + description: 'GraphQL query', + required: true, + }, + { + displayName: 'Variables', + name: 'variables', + type: 'json', + default: {}, + description: 'Query variables', + displayOptions: { + show: { + requestFormat: [ + "json" + ], + requestMethod: [ + "POST" + ], + }, + }, + }, + { + displayName: 'Operation Name', + name: 'operationName', + type: 'string', + default: '', + description: 'Name of operation to execute', + displayOptions: { + show: { + requestFormat: [ + "json" + ], + requestMethod: [ + "POST" + ], + }, + }, + }, + { + displayName: 'Response Format', + name: 'responseFormat', + type: 'options', + options: [ + { + name: 'JSON', + value: 'json' + }, + { + name: 'String', + value: 'string' + }, + ], + default: 'json', + description: 'The format in which the data gets returned from the URL.', + }, + { + displayName: 'Response Data Property Name', + name: 'dataPropertyName', + type: 'string', + default: 'response', + required: true, + displayOptions: { + show: { + responseFormat: [ + 'string', + ], + }, + }, + description: 'Name of the property to which to write the response data.', + }, + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); + + let requestOptions: OptionsWithUri & RequestPromiseOptions; + + const returnItems: INodeExecutionData[] = []; + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const requestMethod = this.getNodeParameter('requestMethod', itemIndex, 'POST') as string; + const endpoint = this.getNodeParameter('endpoint', itemIndex, '') as string; + const requestFormat = this.getNodeParameter('requestFormat', itemIndex, 'graphql') as string; + const responseFormat = this.getNodeParameter('responseFormat', 0) as string; + + requestOptions = { + headers: { + 'content-type': `application/${requestFormat}`, + }, + method: requestMethod, + uri: endpoint, + simple: false, + rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + + const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string; + if (requestMethod === 'GET') { + requestOptions.qs = { + query: gqlQuery + }; + } else { + if (requestFormat === 'json') { + requestOptions.body = { + query: gqlQuery, + variables: this.getNodeParameter('variables', itemIndex, {}) as object, + operationName: this.getNodeParameter('operationName', itemIndex, null) as string, + }; + if (typeof requestOptions.body.variables === "string") { + requestOptions.body.variables = {}; + } + if (requestOptions.body.operationName === "") { + requestOptions.body.operation = null; + } + requestOptions.json = true; + } else { + requestOptions.body = gqlQuery; + } + } + + const response = await this.helpers.request(requestOptions); + if (responseFormat === 'string') { + const dataPropertyName = this.getNodeParameter('dataPropertyName', 0) as string; + + returnItems.push({ + json: { + [dataPropertyName]: response, + } + }); + } else { + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + + returnItems.push({ json: response }); + } + } + + return this.prepareOutputData(returnItems); + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 559b53dade..b216508861 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -80,6 +80,7 @@ "dist/nodes/Gitlab/GitlabTrigger.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", + "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/HttpRequest.node.js", "dist/nodes/If.node.js", "dist/nodes/Interval.node.js", From 971a134cc42b9d6364d604de064c040b9bad989d Mon Sep 17 00:00:00 2001 From: Frane Bandov Date: Wed, 30 Oct 2019 14:02:14 +0100 Subject: [PATCH 2/3] Fix [object Object] issue in interface --- packages/nodes-base/nodes/GraphQL/GraphQL.node.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts index 8c5e438af1..4f7ba55ca7 100644 --- a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts +++ b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts @@ -92,7 +92,7 @@ export class GraphQL implements INodeType { displayName: 'Variables', name: 'variables', type: 'json', - default: {}, + default: '', description: 'Query variables', displayOptions: { show: { @@ -194,7 +194,11 @@ export class GraphQL implements INodeType { operationName: this.getNodeParameter('operationName', itemIndex, null) as string, }; if (typeof requestOptions.body.variables === "string") { - requestOptions.body.variables = {}; + try { + requestOptions.body.variables = JSON.parse(requestOptions.body.variables) + } catch { + requestOptions.body.variables = {}; + } } if (requestOptions.body.operationName === "") { requestOptions.body.operation = null; From d67e77f760a47f38cedab1d3f5f2ee92b84567d7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 31 Oct 2019 11:45:50 +0100 Subject: [PATCH 3/3] :zap: Add GraphQL logo and fix lint issues --- .../nodes-base/nodes/GraphQL/GraphQL.node.ts | 35 +++++++++--------- packages/nodes-base/nodes/GraphQL/graphql.png | Bin 0 -> 3108 bytes 2 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 packages/nodes-base/nodes/GraphQL/graphql.png diff --git a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts index 4f7ba55ca7..85ea50402c 100644 --- a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts +++ b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts @@ -12,6 +12,7 @@ export class GraphQL implements INodeType { description: INodeTypeDescription = { displayName: 'GraphQL', name: 'graphql', + icon: 'file:graphql.png', group: ['input'], version: 1, description: 'Makes a GraphQL request and returns the received data', @@ -29,11 +30,11 @@ export class GraphQL implements INodeType { options: [ { name: 'GET', - value: 'GET' + value: 'GET', }, { name: 'POST', - value: 'POST' + value: 'POST', }, ], default: 'POST', @@ -63,17 +64,17 @@ export class GraphQL implements INodeType { options: [ { name: 'GraphQL (raw)', - value: 'graphql' + value: 'graphql', }, { name: 'JSON', - value: 'json' + value: 'json', }, ], displayOptions: { show: { requestMethod: [ - "POST" + 'POST', ], }, }, @@ -97,10 +98,10 @@ export class GraphQL implements INodeType { displayOptions: { show: { requestFormat: [ - "json" + 'json', ], requestMethod: [ - "POST" + 'POST', ], }, }, @@ -114,10 +115,10 @@ export class GraphQL implements INodeType { displayOptions: { show: { requestFormat: [ - "json" + 'json', ], requestMethod: [ - "POST" + 'POST', ], }, }, @@ -129,11 +130,11 @@ export class GraphQL implements INodeType { options: [ { name: 'JSON', - value: 'json' + value: 'json', }, { name: 'String', - value: 'string' + value: 'string', }, ], default: 'json', @@ -168,7 +169,7 @@ export class GraphQL implements INodeType { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const requestMethod = this.getNodeParameter('requestMethod', itemIndex, 'POST') as string; const endpoint = this.getNodeParameter('endpoint', itemIndex, '') as string; - const requestFormat = this.getNodeParameter('requestFormat', itemIndex, 'graphql') as string; + const requestFormat = this.getNodeParameter('requestFormat', itemIndex, 'graphql') as string; const responseFormat = this.getNodeParameter('responseFormat', 0) as string; requestOptions = { @@ -183,8 +184,8 @@ export class GraphQL implements INodeType { const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string; if (requestMethod === 'GET') { - requestOptions.qs = { - query: gqlQuery + requestOptions.qs = { + query: gqlQuery }; } else { if (requestFormat === 'json') { @@ -193,14 +194,14 @@ export class GraphQL implements INodeType { variables: this.getNodeParameter('variables', itemIndex, {}) as object, operationName: this.getNodeParameter('operationName', itemIndex, null) as string, }; - if (typeof requestOptions.body.variables === "string") { + if (typeof requestOptions.body.variables === 'string') { try { - requestOptions.body.variables = JSON.parse(requestOptions.body.variables) + requestOptions.body.variables = JSON.parse(requestOptions.body.variables); } catch { requestOptions.body.variables = {}; } } - if (requestOptions.body.operationName === "") { + if (requestOptions.body.operationName === '') { requestOptions.body.operation = null; } requestOptions.json = true; diff --git a/packages/nodes-base/nodes/GraphQL/graphql.png b/packages/nodes-base/nodes/GraphQL/graphql.png new file mode 100644 index 0000000000000000000000000000000000000000..64a778195f0b336cec8295c1023f7913c834cb74 GIT binary patch literal 3108 zcmV+<4BPXGP)S`pZX>4PgMw+U{wu%qL`a;D< zltx?9+D2l1#9E87npm5fh_-+rSW~T!Xhc~pE3oX|xifPf{m$GAF1vSE$?gP@2E0?tfg~d>1i(Aio#S?b&gB-XhFOz|(vJE>JrzGU?!3UlWF?c8 z8oPqMPs9BXK$}n_RL{;KrBFy%ZJP(d|K(P=4!AN~a9ydFwA3HnM~oMZfX*3x~3%?h0R zB`$D5#Q|oXZH0S|q`eKl#i2w|L zn}NGN?AWfr&!e#LN7#ukO0p{~+2dHtS2&eN!9kA{tmfgnie=0cHE|5^Ux;fyaKt}# zLa&40Ux0mTj{(NQJ*0iGaUryDH$eRoJHFmLJduRG6+Ga1farua*9jv|7o<9+(JyNj z%Mi+fpf0WmGP$0NTTeB!l(bu8ls?Nz>X0`062-Y@ghmqYBIU z5x##1L3qpg=I+PT-ROAqO@8%|tp}}rl&zfBKj28s_aGG=kHz@Fv>EaFMnu~?+1fv8}>y<;%2o>8QjXwgnX}s@J`7! z!~7u3=MY%Qmj~$VN=$I$l;=+$eZZS${pkttqi?mMjgWXI9RCJdUj(hb>u)PT`Gj*5 zQxU)DSWJY5XW`^^e#Qub5I*1FM_S4r(9+Ba>cLgAu5QqQ_PH6i^^61B7w#AMF?|?E zI&R9UbDkb`lLpumhe|^TO3Q?{Fm-y@3qTU+7WYslYxAApNtC!{v`ws|ZGgQ^kcNh{ z(ss(q+i`>R3b;!x3$sfTnM^eXUTngKAgy~?XfqpIwPEyjc2PlV=m5ebjR3>amSkO! zNGeD|m1&m5kKvcyk&dp5grtZigiFkM6O43Pvc1oaXHafvwb#2Q*uW)U6umqc{F`mJA0x9M!PnnZ$L4EHi zL}o`Ov=>-qr^7V=0;rES#Ja0~QfdQdwgX)Q0huqcqW+w#*y~kTT44>H((hEgja0KD8sH+UvQlu54zur((LWE9V1cPCs>P_F;0pPi&AFW~uEUHxy4W7GLF3l}>2~Z;8>j9S?WjQ?`Ol%Wk&W(t!)M|)cmv?!b3NeBK1KCGL z8b9qu4jVfxy#N0DA_$tz+goe|_Fgq2+Vxc0nrKLE1nH(50NKu}4 z4prU5W|rDWoR8}19O=}F!jzxV9OLwnb-6}VEj3zo3|C7imGRjo&?mqY{{=1Got5F+ z{YMFTxdi^zW~g4#U|=aF@JS5-*81Ug88Dcd%;aTFx^;6HyFeNqac5w}<)LrEh*G&? zBfcrumQj~wY)mE$_LI!vVUevbKR?w@C*F9g_EJ9)orVDaqNpH)(b#6Xdj z8cof9?LH@dhO1ZWC|gKU{t?^BNVL3r)UuHlcInN_EclG$U_Lur3pHOE;f^7O|CWBE zgghB#!}^|=gF!)T1URRyl1#=^P;1Xe7+2TJl$Rj|P3FK{3IWFhBfgadrKQSyhvrWo z9P7T^dmU#;rB`Fp@q&VDvq-Eg0dD7)0i2F| z?F&YxE3vGLo4ck!>Y=LJ7uq-V^adym9a4_<4@lfdV}m0t;NKy*Hw0JC?6bIM0%Mul zl*{4BiI1Y_xLH@bcjvwze`Q@qKlBuc{Rupn9ceb=c$9w66w~L<=X(qrr?yIt$kQk7 zzq<3uQTJgh`50~~A68EAa!CBOQF{oqD=0N|!_L=T=1gpiKH%oWL-Fj-;qSo(b-CSe zg5OKtF0mUv0ISCiXoMhb$*EXSV+t^9?a~T$Ual_Jh4OYJZ*8PO9 znZv9N+FT{A; zu_<&ps+ik0UR@HtY->_8e3jkMRa41ny)Q0#zwttVf3Jf-8GxOZDA84p(DiphWuI~7 z?7OXCuyC)7GQTyjc<|3tISWShWv$VDx~`Zi71ww}ZEym-x0ca)#th`dsEXonK2C^R zcl?+_W%fS6ULh)AYC9lnJoOvw%W?V+cj}a`XUYB{x97jFf{ZMVF~DPh#{iE3-eaHt3orm@`F~ee!juI70000