fix(GraphQL Node): Throw error if GraphQL variables are not objects or strings (#11904)

This commit is contained in:
Dana 2024-12-02 17:15:49 +01:00 committed by GitHub
parent 3814f42ada
commit 85f30b27ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 205 additions and 95 deletions

View file

@ -418,41 +418,50 @@ export class GraphQL implements INodeType {
const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string; const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string;
if (requestMethod === 'GET') { if (requestMethod === 'GET') {
if (!requestOptions.qs) { requestOptions.qs = requestOptions.qs ?? {};
requestOptions.qs = {};
}
requestOptions.qs.query = gqlQuery; requestOptions.qs.query = gqlQuery;
} else { }
if (requestFormat === 'json') { if (requestFormat === 'json') {
const jsonBody = { const variables = this.getNodeParameter('variables', itemIndex, {});
...requestOptions.body,
query: gqlQuery, let parsedVariables;
variables: this.getNodeParameter('variables', itemIndex, {}) as object, if (typeof variables === 'string') {
operationName: this.getNodeParameter('operationName', itemIndex) as string,
};
if (typeof jsonBody.variables === 'string') {
try { try {
jsonBody.variables = JSON.parse(jsonBody.variables || '{}'); parsedVariables = JSON.parse(variables || '{}');
} catch (error) { } catch (error) {
throw new NodeOperationError( throw new NodeOperationError(
this.getNode(), this.getNode(),
'Using variables failed:\n' + `Using variables failed:\n${variables}\n\nWith error message:\n${error}`,
(jsonBody.variables as string) +
'\n\nWith error message:\n' +
(error as string),
{ itemIndex }, { itemIndex },
); );
} }
} else if (typeof variables === 'object' && variables !== null) {
parsedVariables = variables;
} else {
throw new NodeOperationError(
this.getNode(),
`Using variables failed:\n${variables}\n\nGraphQL variables should be either an object or a string.`,
{ itemIndex },
);
} }
const jsonBody = {
...requestOptions.body,
query: gqlQuery,
variables: parsedVariables,
operationName: this.getNodeParameter('operationName', itemIndex) as string,
};
if (jsonBody.operationName === '') { if (jsonBody.operationName === '') {
jsonBody.operationName = null; jsonBody.operationName = null;
} }
requestOptions.json = true; requestOptions.json = true;
requestOptions.body = jsonBody; requestOptions.body = jsonBody;
} else { } else {
requestOptions.body = gqlQuery; requestOptions.body = gqlQuery;
} }
}
let response; let response;
// Now that the options are all set make the actual http request // Now that the options are all set make the actual http request
@ -509,22 +518,19 @@ export class GraphQL implements INodeType {
throw new NodeApiError(this.getNode(), response.errors as JsonObject, { message }); throw new NodeApiError(this.getNode(), response.errors as JsonObject, { message });
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (!this.continueOnFail()) {
throw error;
}
const errorData = this.helpers.returnJsonArray({ const errorData = this.helpers.returnJsonArray({
$error: error, error: error.message,
json: this.getInputData(itemIndex),
itemIndex,
}); });
const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, { const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, {
itemData: { item: itemIndex }, itemData: { item: itemIndex },
}); });
returnItems.push(...exectionErrorWithMetaData); returnItems.push(...exectionErrorWithMetaData);
continue;
}
throw error;
} }
} }
return [returnItems]; return [returnItems];
} }
} }

View file

@ -1,54 +1,83 @@
import type { WorkflowTestData } from '@test/nodes/types'; /* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; import nock from 'nock';
import * as Helpers from '@test/nodes/Helpers';
import {
equalityTest,
getWorkflowFilenames,
initBinaryDataService,
setup,
workflowToTests,
} from '@test/nodes/Helpers';
describe('GraphQL Node', () => { describe('GraphQL Node', () => {
const mockResponse = { const workflows = getWorkflowFilenames(__dirname);
const workflowTests = workflowToTests(workflows);
const baseUrl = 'https://api.n8n.io/';
beforeAll(async () => {
await initBinaryDataService();
nock.disableNetConnect();
nock(baseUrl)
.matchHeader('accept', 'application/json')
.matchHeader('content-type', 'application/json')
.matchHeader('user-agent', 'axios/1.7.4')
.matchHeader('content-length', '263')
.matchHeader('accept-encoding', 'gzip, compress, deflate, br')
.post(
'/graphql',
'{"query":"query {\\n nodes(pagination: { limit: 1 }) {\\n data {\\n id\\n attributes {\\n name\\n displayName\\n description\\n group\\n codex\\n createdAt\\n }\\n }\\n }\\n}","variables":{},"operationName":null}',
)
.reply(200, {
data: { data: {
nodes: {}, nodes: {
}, data: [
}; {
id: '1',
attributes: {
name: 'n8n-nodes-base.activeCampaign',
displayName: 'ActiveCampaign',
description: 'Create and edit data in ActiveCampaign',
group: '["transform"]',
const tests: WorkflowTestData[] = [ codex: {
data: {
details:
'ActiveCampaign is a cloud software platform that allows customer experience automation, which combines email marketing, marketing automation, sales automation, and CRM categories. Use this node when you want to interact with your ActiveCampaign account.',
resources: {
primaryDocumentation: [
{ {
description: 'should run Request Format JSON', url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.activecampaign/',
input: {
workflowData: Helpers.readJsonFileSync('nodes/GraphQL/test/workflow.json'),
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Fetch Request Format JSON': [
[
{
json: mockResponse,
}, },
], ],
credentialDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/credentials/activeCampaign/',
},
], ],
}, },
categories: ['Marketing'],
nodeVersion: '1.0',
codexVersion: '1.0',
},
},
createdAt: '2019-08-30T22:54:39.934Z',
}, },
nock: {
baseUrl: 'https://api.n8n.io',
mocks: [
{
method: 'post',
path: '/graphql',
statusCode: 200,
responseBody: mockResponse,
}, },
], ],
}, },
}, },
];
const nodeTypes = Helpers.setup(tests);
test.each(tests)('$description', async (testData) => {
const { result } = await executeWorkflow(testData, nodeTypes);
const resultNodeData = Helpers.getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) =>
expect(resultData).toEqual(testData.output.nodeData[nodeName]),
);
expect(result.finished).toEqual(true);
}); });
}); });
afterAll(() => {
nock.restore();
});
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});

View file

@ -0,0 +1,32 @@
{
"meta": {
"templateId": "216",
"instanceId": "ee90fdf8d57662f949e6c691dc07fa0fd2f66e1eee28ed82ef06658223e67255"
},
"nodes": [
{
"parameters": {
"endpoint": "https://graphql-teas-endpoint.netlify.app/",
"requestFormat": "json",
"query": "query getAllTeas($name: String) {\n teas(name: $name) {\n name,\n id\n }\n}",
"variables": "={{ 1 }}"
},
"id": "7aece03f-e0d9-4f49-832c-fc6465613ca7",
"name": "Test: Errors on unsuccessful Expression validation",
"type": "n8n-nodes-base.graphql",
"typeVersion": 1,
"position": [660, 200],
"onError": "continueRegularOutput"
}
],
"connections": {},
"pinData": {
"Test: Errors on unsuccessful Expression validation": [
{
"json": {
"error": "Using variables failed:\n1\n\nGraphQL variables should be either an object or a string."
}
}
]
}
}

View file

@ -1,16 +1,16 @@
{ {
"meta": { "meta": {
"templateCredsSetupCompleted": true, "templateId": "216",
"instanceId": "104a4d08d8897b8bdeb38aaca515021075e0bd8544c983c2bb8c86e6a8e6081c" "instanceId": "ee90fdf8d57662f949e6c691dc07fa0fd2f66e1eee28ed82ef06658223e67255"
}, },
"nodes": [ "nodes": [
{ {
"parameters": {}, "parameters": {},
"id": "fb826323-2e48-4f11-bb0e-e12de32e22ee", "id": "5e2ef15b-2c6c-412f-a9da-515b5211386e",
"name": "When clicking Test workflow", "name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [180, 160] "position": [420, 100]
}, },
{ {
"parameters": { "parameters": {
@ -21,8 +21,8 @@
"name": "Fetch Request Format JSON", "name": "Fetch Request Format JSON",
"type": "n8n-nodes-base.graphql", "type": "n8n-nodes-base.graphql",
"typeVersion": 1, "typeVersion": 1,
"position": [420, 160], "position": [700, 140],
"id": "7f8ceaf4-b82f-48d5-be0b-9fe3bfb35ee4" "id": "e1c750a0-8d6c-4e81-8111-3218e1e6e69f"
} }
], ],
"connections": { "connections": {
@ -38,5 +38,48 @@
] ]
} }
}, },
"pinData": {} "pinData": {
"Fetch Request Format JSON": [
{
"json": {
"data": {
"nodes": {
"data": [
{
"id": "1",
"attributes": {
"name": "n8n-nodes-base.activeCampaign",
"displayName": "ActiveCampaign",
"description": "Create and edit data in ActiveCampaign",
"group": "[\"transform\"]",
"codex": {
"data": {
"details": "ActiveCampaign is a cloud software platform that allows customer experience automation, which combines email marketing, marketing automation, sales automation, and CRM categories. Use this node when you want to interact with your ActiveCampaign account.",
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.activecampaign/"
}
],
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/activeCampaign/"
}
]
},
"categories": ["Marketing"],
"nodeVersion": "1.0",
"codexVersion": "1.0"
}
},
"createdAt": "2019-08-30T22:54:39.934Z"
}
}
]
}
}
}
}
]
}
} }