diff --git a/package-lock.json b/package-lock.json index fb56468cc..531e4d381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8943,7 +8943,6 @@ "version": "16.12.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", - "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -15554,6 +15553,9 @@ "name": "@probo/n8n-nodes-probo", "version": "0.0.1", "license": "ISC", + "dependencies": { + "graphql": "^16.12.0" + }, "devDependencies": { "@n8n/node-cli": "^0.17.0", "eslint": "9.32.0" diff --git a/packages/n8n-node/nodes/Probo/Probo.node.json b/packages/n8n-node/nodes/Probo/Probo.node.json new file mode 100644 index 000000000..5aba2f279 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/Probo.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-probo", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development", "Developer Tools", "Automation"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://www.getprobo.com/docs" + } + ], + "primaryDocumentation": [ + { + "url": "https://www.getprobo.com/docs" + } + ], + "generic": [] + } +} diff --git a/packages/n8n-node/nodes/Probo/Probo.node.ts b/packages/n8n-node/nodes/Probo/Probo.node.ts new file mode 100644 index 000000000..c73886187 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/Probo.node.ts @@ -0,0 +1,117 @@ +import { + NodeConnectionTypes, + type IExecuteFunctions, + type INodeExecutionData, + type INodeType, + type INodeTypeDescription, +} from 'n8n-workflow'; +import { executeOperation } from './operations'; + +export class Probo implements INodeType { + description: INodeTypeDescription = { + displayName: 'Probo', + name: 'probo', + icon: { light: 'file:../../icons/probo.svg', dark: 'file:../../icons/probo.svg' }, + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"]}}', + description: 'Consume data from the Probo API', + defaults: { + name: 'Probo', + }, + usableAsTool: true, + inputs: [NodeConnectionTypes.Main], + outputs: [NodeConnectionTypes.Main], + credentials: [ + { + name: 'proboApi', + required: true, + displayOptions: { + show: { + authentication: ['apiKey'], + }, + }, + }, + ], + requestDefaults: { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + ], + default: 'apiKey', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Execute', + value: 'execute', + description: 'Execute a GraphQL query or mutation', + action: 'Execute a GraphQL operation', + }, + ], + default: 'execute', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + rows: 5, + }, + displayOptions: { + show: { + operation: ['execute'], + }, + }, + default: '', + description: 'The complete GraphQL operation including operation name and variable declarations (e.g., "query GetUser($userId: ID!) { node(id: $userId) { id } }" or "mutation UpdateUser($input: UpdateUserInput!) { updateUser(input: $input) { id } }")', + required: true, + }, + { + displayName: 'Variables', + name: 'variables', + type: 'json', + displayOptions: { + show: { + operation: ['execute'], + }, + }, + default: '{}', + description: 'GraphQL variables as JSON object', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + const operation = this.getNodeParameter('operation', i) as string; + const result = await executeOperation.call(this, operation, i); + returnData.push(result); + } + + return [returnData]; + } + + methods = { + listSearch: {}, + }; +} \ No newline at end of file diff --git a/packages/n8n-node/nodes/Probo/operations/execute.ts b/packages/n8n-node/nodes/Probo/operations/execute.ts new file mode 100644 index 000000000..c0da3119f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/operations/execute.ts @@ -0,0 +1,81 @@ +import { parse, getOperationAST, type DocumentNode } from 'graphql'; +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; + +export interface GraphQLParameters { + query: string; + variables?: string; +} + +export async function executeGraphQL( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const query = this.getNodeParameter('query', itemIndex) as string; + const variablesParam = this.getNodeParameter('variables', itemIndex) as string; + + let document: DocumentNode; + try { + document = parse(query); + } catch (error) { + throw new Error(`Invalid GraphQL operation: ${error instanceof Error ? error.message : String(error)}`); + } + + const operationAST = getOperationAST(document); + if (!operationAST) { + throw new Error('GraphQL operation must contain a query, mutation, or subscription'); + } + + if (!operationAST.name) { + throw new Error('GraphQL operation must have a name (e.g., "query GetUser { ... }" or "mutation UpdateUser { ... }")'); + } + + const operationName = operationAST.name.value; + + let variables = {}; + if (variablesParam) { + try { + variables = + typeof variablesParam === 'string' ? JSON.parse(variablesParam) : variablesParam; + } catch (error) { + throw new Error(`Invalid JSON in Variables: ${error}`); + } + } + + const body: { + query: string; + operationName?: string; + variables?: Record; + } = { + query, + operationName, + }; + + if (Object.keys(variables).length > 0) { + body.variables = variables; + } + + const credentials = await this.getCredentials('proboApi'); + + if (!credentials!.apiKey) { + throw new Error('API Key is required'); + } + + const responseData = await this.helpers.httpRequest({ + method: 'POST', + baseURL: `${credentials!.server}`, + url: '/api/console/v1/query', + headers: { + Authorization: `Bearer ${credentials!.apiKey}`, + 'Content-Type': 'application/json', + }, + body: body, + json: true, + returnFullResponse: true, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} + diff --git a/packages/n8n-node/nodes/Probo/operations/index.ts b/packages/n8n-node/nodes/Probo/operations/index.ts new file mode 100644 index 000000000..934e4a5f8 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/operations/index.ts @@ -0,0 +1,16 @@ +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { executeGraphQL } from './execute'; + +export async function executeOperation( + this: IExecuteFunctions, + operation: string, + itemIndex: number, +): Promise { + switch (operation) { + case 'execute': + return executeGraphQL.call(this, itemIndex); + default: + throw new Error(`Unknown operation: ${operation}`); + } +} + diff --git a/packages/n8n-node/package.json b/packages/n8n-node/package.json index 9ca17f94d..82961071a 100644 --- a/packages/n8n-node/package.json +++ b/packages/n8n-node/package.json @@ -31,5 +31,8 @@ }, "peerDependencies": { "n8n-workflow": "*" + }, + "dependencies": { + "graphql": "^16.12.0" } }