Add query action

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-01 15:38:02 +01:00
parent e494eb8807
commit 871ce23dc8
6 changed files with 239 additions and 1 deletions

4
package-lock.json generated
View File

@@ -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"

View File

@@ -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": []
}
}

View File

@@ -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<INodeExecutionData[][]> {
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: {},
};
}

View File

@@ -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<INodeExecutionData> {
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<string, any>;
} = {
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 },
};
}

View File

@@ -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<INodeExecutionData> {
switch (operation) {
case 'execute':
return executeGraphQL.call(this, itemIndex);
default:
throw new Error(`Unknown operation: ${operation}`);
}
}

View File

@@ -31,5 +31,8 @@
},
"peerDependencies": {
"n8n-workflow": "*"
},
"dependencies": {
"graphql": "^16.12.0"
}
}