pull:初次提交

This commit is contained in:
Yep_Q
2025-09-08 04:48:28 +08:00
parent 5c0619656d
commit f64f498365
11751 changed files with 1953723 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
{
"node": "n8n-nodes-base.graphql",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage", "Development"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.graphql/"
}
],
"generic": [
{
"label": "What are APIs and how to use them with no code",
"icon": " 🪢",
"url": "https://n8n.io/blog/what-are-apis-how-to-use-them-with-no-code/"
},
{
"label": "How to automatically give kudos to contributors with GitHub, Slack, and n8n",
"icon": "👏",
"url": "https://n8n.io/blog/how-to-automatically-give-kudos-to-contributors-with-github-slack-and-n8n/"
},
{
"label": "How Goomer automated their operations with over 200 n8n workflows",
"icon": "🛵",
"url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/"
}
]
}
}

View File

@@ -0,0 +1,576 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
JsonObject,
IRequestOptionsSimplified,
IRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError, NodeConnectionTypes, NodeOperationError, jsonParse } from 'n8n-workflow';
export class GraphQL implements INodeType {
description: INodeTypeDescription = {
displayName: 'GraphQL',
name: 'graphql',
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
icon: 'file:graphql.png',
group: ['input'],
version: [1, 1.1],
description: 'Makes a GraphQL request and returns the received data',
defaults: {
name: 'GraphQL',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
authentication: ['basicAuth'],
},
},
},
{
name: 'httpCustomAuth',
required: true,
displayOptions: {
show: {
authentication: ['customAuth'],
},
},
},
{
name: 'httpDigestAuth',
required: true,
displayOptions: {
show: {
authentication: ['digestAuth'],
},
},
},
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
authentication: ['headerAuth'],
},
},
},
{
name: 'httpQueryAuth',
required: true,
displayOptions: {
show: {
authentication: ['queryAuth'],
},
},
},
{
name: 'oAuth1Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth1'],
},
},
},
{
name: 'oAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'Custom Auth',
value: 'customAuth',
},
{
name: 'Digest Auth',
value: 'digestAuth',
},
{
name: 'Header Auth',
value: 'headerAuth',
},
{
name: 'None',
value: 'none',
},
{
name: 'OAuth1',
value: 'oAuth1',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'Query Auth',
value: 'queryAuth',
},
],
default: 'none',
description: 'The way to authenticate',
},
{
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 (Insecure)',
name: 'allowUnauthorizedCerts',
type: 'boolean',
default: false,
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues
description:
'Whether to download 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'],
'@version': [1],
},
},
default: 'graphql',
description: 'The format for the query payload',
},
{
displayName: 'Request Format',
name: 'requestFormat',
type: 'options',
required: true,
options: [
{
name: 'JSON (Recommended)',
value: 'json',
description:
'JSON object with query, variables, and operationName properties. The standard and most widely supported format for GraphQL requests.',
},
{
name: 'GraphQL (Raw)',
value: 'graphql',
description:
'Raw GraphQL query string. Not all servers support this format. Use JSON for better compatibility.',
},
],
displayOptions: {
show: {
requestMethod: ['POST'],
'@version': [{ _cnd: { gte: 1.1 } }],
},
},
default: 'json',
description: 'The request format for the query payload',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: 'GraphQL query',
required: true,
typeOptions: {
rows: 6,
},
},
{
displayName: 'Variables',
name: 'variables',
type: 'json',
default: '',
description: 'Query variables as JSON object',
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: 'data',
required: true,
displayOptions: {
show: {
responseFormat: ['string'],
},
},
description: 'Name of the property to which to write the response data',
},
// Header Parameters
{
displayName: 'Headers',
name: 'headerParametersUi',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'The headers to send',
default: {},
options: [
{
name: 'parameter',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header',
},
],
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
let httpBasicAuth;
let httpDigestAuth;
let httpCustomAuth;
let httpHeaderAuth;
let httpQueryAuth;
let oAuth1Api;
let oAuth2Api;
try {
httpBasicAuth = await this.getCredentials('httpBasicAuth');
} catch (error) {
// Do nothing
}
try {
httpCustomAuth = await this.getCredentials('httpCustomAuth');
} catch (error) {
// Do nothing
}
try {
httpDigestAuth = await this.getCredentials('httpDigestAuth');
} catch (error) {
// Do nothing
}
try {
httpHeaderAuth = await this.getCredentials('httpHeaderAuth');
} catch (error) {
// Do nothing
}
try {
httpQueryAuth = await this.getCredentials('httpQueryAuth');
} catch (error) {
// Do nothing
}
try {
oAuth1Api = await this.getCredentials('oAuth1Api');
} catch (error) {
// Do nothing
}
try {
oAuth2Api = await this.getCredentials('oAuth2Api');
} catch (error) {
// Do nothing
}
let requestOptions: IRequestOptions;
const returnItems: INodeExecutionData[] = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
const requestMethod = this.getNodeParameter(
'requestMethod',
itemIndex,
'POST',
) as IHttpRequestMethods;
const endpoint = this.getNodeParameter('endpoint', itemIndex, '') as string;
const requestFormat = this.getNodeParameter('requestFormat', itemIndex, 'json') as string;
const responseFormat = this.getNodeParameter('responseFormat', 0) as string;
const { parameter }: { parameter?: Array<{ name: string; value: string }> } =
this.getNodeParameter('headerParametersUi', itemIndex, {}) as IDataObject;
const headerParameters = (parameter || []).reduce(
(result, item) => ({
...result,
[item.name]: item.value,
}),
{},
);
requestOptions = {
headers: {
'content-type': `application/${requestFormat}`,
...headerParameters,
},
method: requestMethod,
uri: endpoint,
simple: false,
rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false),
};
// Add credentials if any are set
if (httpBasicAuth !== undefined) {
requestOptions.auth = {
user: httpBasicAuth.user as string,
pass: httpBasicAuth.password as string,
};
}
if (httpCustomAuth !== undefined) {
const customAuth = jsonParse<IRequestOptionsSimplified>(
(httpCustomAuth.json as string) || '{}',
{ errorMessage: 'Invalid Custom Auth JSON' },
);
if (customAuth.headers) {
requestOptions.headers = { ...requestOptions.headers, ...customAuth.headers };
}
if (customAuth.body) {
requestOptions.body = { ...requestOptions.body, ...customAuth.body };
}
if (customAuth.qs) {
requestOptions.qs = { ...requestOptions.qs, ...customAuth.qs };
}
}
if (httpHeaderAuth !== undefined) {
requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value;
}
if (httpQueryAuth !== undefined) {
if (!requestOptions.qs) {
requestOptions.qs = {};
}
requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value;
}
if (httpDigestAuth !== undefined) {
requestOptions.auth = {
user: httpDigestAuth.user as string,
pass: httpDigestAuth.password as string,
sendImmediately: false,
};
}
const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string;
if (requestMethod === 'GET') {
requestOptions.qs = requestOptions.qs ?? {};
requestOptions.qs.query = gqlQuery;
}
if (requestFormat === 'json') {
const variables = this.getNodeParameter('variables', itemIndex, {});
let parsedVariables;
if (typeof variables === 'string') {
try {
parsedVariables = JSON.parse(variables || '{}');
} catch (error) {
throw new NodeOperationError(
this.getNode(),
`Using variables failed:\n${variables}\n\nWith error message:\n${error}`,
{ 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 === '') {
jsonBody.operationName = null;
}
requestOptions.json = true;
requestOptions.body = jsonBody;
} else {
requestOptions.body = gqlQuery;
}
let response;
// Now that the options are all set make the actual http request
if (oAuth1Api !== undefined) {
response = await this.helpers.requestOAuth1.call(this, 'oAuth1Api', requestOptions);
} else if (oAuth2Api !== undefined) {
response = await this.helpers.requestOAuth2.call(
this,
'oAuth2Api',
{
...requestOptions,
// needed for the refresh mechanism to work properly
resolveWithFullResponse: true,
},
{
tokenType: 'Bearer',
},
);
// since we are using `resolveWithFullResponse: true`, we need to grab the body
response = response.body;
} else {
response = await this.helpers.request(requestOptions);
}
if (responseFormat === 'string') {
const dataPropertyName = this.getNodeParameter('dataPropertyName', 0);
returnItems.push({
json: {
[dataPropertyName]: response,
},
});
} else {
if (typeof response === 'string') {
try {
response = JSON.parse(response);
} catch (error) {
throw new NodeOperationError(
this.getNode(),
'Response body is not valid JSON. Change "Response Format" to "String"',
{ itemIndex },
);
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject),
{ itemData: { item: itemIndex } },
);
returnItems.push(...executionData);
}
// parse error string messages
if (typeof response === 'string' && response.startsWith('{"errors":')) {
try {
const errorResponse = JSON.parse(response) as IDataObject;
if (Array.isArray(errorResponse.errors)) {
response = errorResponse;
}
} catch (e) {}
}
// throw from response object.errors[]
if (typeof response === 'object' && response.errors) {
const message =
response.errors?.map((error: IDataObject) => error.message).join(', ') ||
'Unexpected error';
throw new NodeApiError(this.getNode(), response.errors as JsonObject, { message });
}
} catch (error) {
if (!this.continueOnFail()) {
throw error;
}
const errorData = this.helpers.returnJsonArray({
error: error.message,
});
const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, {
itemData: { item: itemIndex },
});
returnItems.push(...exectionErrorWithMetaData);
}
}
return [returnItems];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

View File

@@ -0,0 +1,113 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('GraphQL Node', () => {
describe('valid request', () => {
const baseUrl = 'https://api.n8n.io/';
nock(baseUrl)
.matchHeader('accept', 'application/json')
.matchHeader('content-type', 'application/json')
.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: [
{
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',
},
},
],
},
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['workflow.json'],
});
});
describe('invalid expression', () => {
new NodeTestHarness().setupTests({
workflowFiles: ['workflow.error_invalid_expression.json'],
});
});
describe('oauth2 refresh token', () => {
const credentials = {
oAuth2Api: {
scope: '',
accessTokenUrl: 'http://test/token',
clientId: 'dummy_client_id',
clientSecret: 'dummy_client_secret',
oauthTokenData: {
access_token: 'dummy_access_token',
refresh_token: 'dummy_refresh_token',
},
},
};
const baseUrl = 'http://test';
nock(baseUrl)
.post('/graphql', '{"query":"query { foo }","variables":{},"operationName":null}')
.reply(401, {
errors: [
{
message: 'Unauthorized',
},
],
});
nock(baseUrl)
.post('/token', {
refresh_token: 'dummy_refresh_token',
grant_type: 'refresh_token',
})
.reply(200, {
access_token: 'dummy_access_token',
refresh_token: 'dummy_refresh_token',
expires_in: 3600,
});
nock(baseUrl)
.post('/graphql', '{"query":"query { foo }","variables":{},"operationName":null}')
.reply(200, {
data: {
foo: 'bar',
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['workflow.refresh_token.json'],
credentials,
});
});
});

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

@@ -0,0 +1,85 @@
{
"meta": {
"templateId": "216",
"instanceId": "ee90fdf8d57662f949e6c691dc07fa0fd2f66e1eee28ed82ef06658223e67255"
},
"nodes": [
{
"parameters": {},
"id": "5e2ef15b-2c6c-412f-a9da-515b5211386e",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [420, 100]
},
{
"parameters": {
"endpoint": "https://api.n8n.io/graphql",
"requestFormat": "json",
"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}"
},
"name": "Fetch Request Format JSON",
"type": "n8n-nodes-base.graphql",
"typeVersion": 1,
"position": [700, 140],
"id": "e1c750a0-8d6c-4e81-8111-3218e1e6e69f"
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Fetch Request Format JSON",
"type": "main",
"index": 0
}
]
]
}
},
"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"
}
}
]
}
}
}
}
]
}
}

View File

@@ -0,0 +1,67 @@
{
"name": "GraphQL Refersh Token",
"nodes": [
{
"parameters": {},
"id": "ae67b437-48c0-4c4b-9c8f-005b8911ca5f",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [32, 80]
},
{
"parameters": {
"authentication": "oAuth2",
"endpoint": "http://test/graphql",
"requestFormat": "json",
"query": "query { foo }"
},
"name": "GraphQL",
"type": "n8n-nodes-base.graphql",
"typeVersion": 1,
"position": [256, 80],
"id": "6cb4c117-907a-40b9-b3ba-cd3756b1cb7b",
"credentials": {
"oAuth2Api": {
"id": "PpmTbnw41Q2nqqoW",
"name": "Dummy (local)"
}
}
}
],
"pinData": {
"GraphQL": [
{
"json": {
"data": {
"foo": "bar"
}
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "GraphQL",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "659c76f4-ac29-4c2f-bf40-70b476afbc1d",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "eeda9e3069aca300d1dfceeb64beb5b53d715db44a50461bbc5cb0cf6daa01e3"
},
"id": "O4IzQ2D7h7cfZtB4",
"tags": []
}