n8n-nodes-velatir
Version:
n8n community node for Velatir - Human-in-the-loop AI function approval
191 lines (190 loc) • 8.31 kB
JavaScript
;
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;