UNPKG

n8n-nodes-velatir

Version:

n8n community node for Velatir - Human-in-the-loop AI function approval

191 lines (190 loc) 8.31 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Velatir = void 0; const n8n_workflow_1 = require("n8n-workflow"); // Utility function to create a delay without using setTimeout directly function delay(ms) { return new Promise((resolve) => { const start = Date.now(); const check = () => { if (Date.now() - start >= ms) { resolve(); } else { // Use setImmediate for non-blocking polling setImmediate(check); } }; check(); }); } class Velatir { constructor() { this.description = { displayName: 'Velatir', name: 'velatir', icon: 'file:velatir.svg', group: ['transform'], version: 1, description: 'Human approval gate - pauses workflow until approved by a human', defaults: { name: 'Velatir', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [ { name: 'velatirApi', required: true, }, ], properties: [ { displayName: 'Function Name', name: 'functionName', type: 'string', default: '={{$node.name}}', description: 'Name shown to approvers (defaults to node name)', placeholder: 'Send Email', }, { displayName: 'Description', name: 'description', type: 'string', default: '', description: 'Description of what this step does', placeholder: 'Send welcome email to new customers', }, { displayName: 'Polling Interval (Seconds)', name: 'pollingInterval', type: 'number', default: 5, description: 'How often to check for approval (seconds)', typeOptions: { minValue: 1, maxValue: 300, }, }, { displayName: 'Timeout (Minutes)', name: 'timeoutMinutes', type: 'number', default: 10, description: 'Maximum time to wait for approval (0 for unlimited)', typeOptions: { minValue: 0, }, }, ], }; } async execute() { var _a, _b; const items = this.getInputData(); const returnData = []; const functionName = this.getNodeParameter('functionName', 0); const description = this.getNodeParameter('description', 0); const pollingInterval = this.getNodeParameter('pollingInterval', 0); const timeoutMinutes = this.getNodeParameter('timeoutMinutes', 0); const maxAttempts = timeoutMinutes > 0 ? Math.ceil((timeoutMinutes * 60) / pollingInterval) : 0; for (let i = 0; i < items.length; i++) { try { const inputData = items[i].json; const credentials = await this.getCredentials('velatirApi'); // Create watch request with input data as arguments const requestBody = { functionname: functionName || `Step ${i + 1}`, args: inputData, doc: description || `Approve data processing in n8n workflow`, metadata: { nodeType: 'n8n-velatir-gate', workflowId: this.getWorkflow().id, executionId: this.getExecutionId(), nodeId: this.getNode().id, itemIndex: i, }, }; // Create the watch request const createResponse = await this.helpers.httpRequest.call(this, { method: 'POST', url: `${credentials.domain}/api/v1/watches`, body: requestBody, json: true, headers: { 'X-API-Key': credentials.apiKey, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); // API returns data directly in response body if (!createResponse || !createResponse.requestId) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid API response: ${JSON.stringify(createResponse)}`, { itemIndex: i }); } const requestId = createResponse.requestId; const initialState = createResponse.state; // If already approved, return immediately if (initialState === 'approved') { returnData.push({ json: inputData, // Pass through original data unchanged pairedItem: { item: i }, }); continue; } // If denied, throw error if (initialState === 'denied') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Request was denied by Velatir approver (request_id: ${requestId})`, { itemIndex: i }); } // Wait for approval (polling) let attempts = 0; let finalState = initialState; let reason = ""; while (finalState === 'pending') { if (maxAttempts > 0 && attempts >= maxAttempts) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Approval timeout after ${timeoutMinutes} minutes (request_id: ${requestId})`, { itemIndex: i }); } // Wait before next poll await delay(pollingInterval * 1000); attempts++; // Check status const statusResponse = await this.helpers.httpRequest.call(this, { method: 'GET', url: `${credentials.domain}/api/v1/watches/${requestId}`, json: true, headers: { 'X-API-Key': credentials.apiKey, 'Accept': 'application/json', }, }); finalState = statusResponse.state; reason = (_b = (_a = statusResponse.result) === null || _a === void 0 ? void 0 : _a.reason) !== null && _b !== void 0 ? _b : "No reason provided"; } // Handle final decision if (finalState === 'approved') { returnData.push({ json: inputData, // Pass through original data unchanged pairedItem: { item: i }, }); } else if (finalState === 'declined') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Request was denied by Velatir (reason: ${reason}, request_id: ${requestId})`, { itemIndex: i }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unexpected state: ${finalState} (request_id: ${requestId})`, { itemIndex: i }); } } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { error: error instanceof Error ? error.message : 'Unknown error' }, pairedItem: { item: i }, }); continue; } throw error; } } return [returnData]; } } exports.Velatir = Velatir;