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,40 +418,49 @@ 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') {
const jsonBody = { if (requestFormat === 'json') {
...requestOptions.body, const variables = this.getNodeParameter('variables', itemIndex, {});
query: gqlQuery,
variables: this.getNodeParameter('variables', itemIndex, {}) as object, let parsedVariables;
operationName: this.getNodeParameter('operationName', itemIndex) as string, if (typeof variables === 'string') {
}; try {
if (typeof jsonBody.variables === 'string') { parsedVariables = JSON.parse(variables || '{}');
try { } catch (error) {
jsonBody.variables = JSON.parse(jsonBody.variables || '{}'); throw new NodeOperationError(
} catch (error) { this.getNode(),
throw new NodeOperationError( `Using variables failed:\n${variables}\n\nWith error message:\n${error}`,
this.getNode(), { itemIndex },
'Using variables failed:\n' + );
(jsonBody.variables as string) +
'\n\nWith error message:\n' +
(error as string),
{ itemIndex },
);
}
} }
if (jsonBody.operationName === '') { } else if (typeof variables === 'object' && variables !== null) {
jsonBody.operationName = null; parsedVariables = variables;
}
requestOptions.json = true;
requestOptions.body = jsonBody;
} else { } else {
requestOptions.body = gqlQuery; 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 === '') {
jsonBody.operationName = null;
}
requestOptions.json = true;
requestOptions.body = jsonBody;
} else {
requestOptions.body = gqlQuery;
} }
let response; let response;
@ -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()) {
const errorData = this.helpers.returnJsonArray({ throw error;
$error: error,
json: this.getInputData(itemIndex),
itemIndex,
});
const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, {
itemData: { item: itemIndex },
});
returnItems.push(...exectionErrorWithMetaData);
continue;
} }
throw error;
const errorData = this.helpers.returnJsonArray({
error: error.message,
});
const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, {
itemData: { item: itemIndex },
});
returnItems.push(...exectionErrorWithMetaData);
} }
} }
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);
data: { const workflowTests = workflowToTests(workflows);
nodes: {},
},
};
const tests: WorkflowTestData[] = [ const baseUrl = 'https://api.n8n.io/';
{
description: 'should run Request Format JSON', beforeAll(async () => {
input: { await initBinaryDataService();
workflowData: Helpers.readJsonFileSync('nodes/GraphQL/test/workflow.json'), nock.disableNetConnect();
},
output: { nock(baseUrl)
nodeExecutionOrder: ['Start'], .matchHeader('accept', 'application/json')
nodeData: { .matchHeader('content-type', 'application/json')
'Fetch Request Format 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: {
nodes: {
data: [
{ {
json: mockResponse, 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',
},
}, },
], ],
],
},
},
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"
}
}
]
}
}
}
}
]
}
} }