auto-builder-sdk
Version:
SDK for building Auto Builder workflow plugins
675 lines (674 loc) โข 28.1 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-var-requires */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTempFileMetadata = exports.deleteTempFile = exports.readTempFile = exports.createTempFile = exports.listCredentialDefinitions = exports.getCredentialDef = exports.registerCredential = exports.definePlugin = exports.validatePlugin = exports.createFormattedErrorResult = exports.createFormattedResult = exports.createNamespacedResult = exports.getStableOutputKey = exports.getWorkflowNodePosition = exports.BaseNodeExecutor = exports.ParameterResolver = exports.NodeApiError = exports.extractNamespacedData = exports.extractInputData = exports.NodeOperationError = exports.log = void 0;
const credentialRegistry = {};
// Lightweight noop logger compatible with SDK API
exports.log = {
debug: console.debug.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
// Simple error wrappers so plugins can throw the expected error types
class NodeOperationError extends Error {
node;
constructor(node, message) {
super(message);
this.name = 'NodeOperationError';
this.node = node;
}
}
exports.NodeOperationError = NodeOperationError;
/**
* Backward compatibility utility to extract input data from both old and new formats
* Handles the transition from flat JSON structure to namespaced structure
*/
const extractInputData = (inputData, fallbackToLegacy = true) => {
if (!inputData || inputData.length === 0) {
return {};
}
const firstItem = inputData[0]?.json || {};
// Check if this is new namespaced format by looking for metadata indicators
const hasNamespacedData = Object.keys(firstItem).some(key => typeof firstItem[key] === 'object' &&
firstItem[key] !== null &&
firstItem[key]?._metadata?.nodeType);
if (hasNamespacedData && fallbackToLegacy) {
// Extract all non-namespaced properties (legacy data preserved at root level)
const legacyData = {};
Object.keys(firstItem).forEach(key => {
// Skip namespaced objects (they have _metadata.nodeType)
if (typeof firstItem[key] !== 'object' ||
firstItem[key] === null ||
!firstItem[key]?._metadata?.nodeType) {
legacyData[key] = firstItem[key];
}
});
return legacyData;
}
// Return original data (either old format or explicitly requested new format)
return firstItem;
};
exports.extractInputData = extractInputData;
/**
* Extract data from a specific namespace in the new format
* Used when you specifically want data from a particular node type
*/
const extractNamespacedData = (inputData, nodeType) => {
if (!inputData || inputData.length === 0) {
return {};
}
const firstItem = inputData[0]?.json || {};
// Look for the specific namespace
for (const value of Object.values(firstItem)) {
if (typeof value === 'object' &&
value !== null &&
value?._metadata?.nodeType === nodeType) {
return value;
}
}
return {};
};
exports.extractNamespacedData = extractNamespacedData;
class NodeApiError extends Error {
httpStatusCode;
constructor(node, message, opts) {
super(message);
this.name = 'NodeApiError';
this.httpStatusCode = opts?.httpStatusCode;
}
}
exports.NodeApiError = NodeApiError;
// Minimal ParameterResolver with only the public static `resolve` method
class ParameterResolver {
static deepClone(obj) {
if (obj === null || typeof obj !== 'object')
return obj;
if (obj instanceof Date)
return new Date(obj);
if (Array.isArray(obj))
return obj.map((item) => this.deepClone(item));
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key))
cloned[key] = this.deepClone(obj[key]);
}
return cloned;
}
static getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => (current && current[key] !== undefined ? current[key] : undefined), obj);
}
static resolveStringTemplate(template, inputData, context) {
if (!template.includes('{{') || !template.includes('}}'))
return template;
let resolved = template;
resolved = resolved.replace(/\{\{\$json\.([^}]+)\}\}/g, (_, p) => {
const val = this.getNestedValue(inputData, p);
return val !== undefined ? String(val) : _;
});
resolved = resolved.replace(/\{\{\$now\}\}/g, new Date().toISOString());
resolved = resolved.replace(/\{\{new Date\(\)\.toISOString\(\)\}\}/g, new Date().toISOString());
resolved = resolved.replace(/\{\{\$item\}\}/g, JSON.stringify(inputData));
resolved = resolved.replace(/\{\{\$execution\.id\}\}/g, context.executionId || '');
resolved = resolved.replace(/\{\{\$workflow\.id\}\}/g, context.workflowId || '');
return resolved;
}
static resolveTemplates(obj, inputData, context) {
if (typeof obj === 'string')
return this.resolveStringTemplate(obj, inputData, context);
if (Array.isArray(obj))
return obj.map((i) => this.resolveTemplates(i, inputData, context));
if (obj && typeof obj === 'object') {
const out = {};
for (const [k, v] of Object.entries(obj))
out[k] = this.resolveTemplates(v, inputData, context);
return out;
}
return obj;
}
static resolve(parameters, context) {
if (!parameters)
return {};
const cloned = this.deepClone(parameters);
const inputData = context.inputData?.[context.itemIndex || 0]?.json ?? {};
return this.resolveTemplates(cloned, inputData, context);
}
}
exports.ParameterResolver = ParameterResolver;
// Basic BaseNodeExecutor implementation sufficient for most plugins
class BaseNodeExecutor {
resolveParameters(parameters, context) {
return ParameterResolver.resolve(parameters, context);
}
async getCredentials(credentialId) {
if (!credentialId)
return {};
// Fast path: SAFE_MODE sandbox injects credentials into global.__cred
try {
const credMap = globalThis.__cred;
if (credMap && credMap[credentialId]) {
return credMap[credentialId];
}
}
catch {
/* ignore */
}
// vm2 exposes sandbox keys as global variables, which may not be
// attached to `globalThis`. Fallback to a direct free-variable lookup.
// The `typeof` guard prevents ReferenceError if __cred is undefined.
try {
// eslint-disable-next-line no-undef
if (typeof globalThis.__cred !== 'undefined' && globalThis.__cred && globalThis.__cred[credentialId]) {
// eslint-disable-next-line no-undef
return globalThis.__cred[credentialId];
}
}
catch {
/* ignore */
}
const candidates = [
'auto-builder/dist/services/CredentialManager.js',
(() => {
const path = require('node:path');
return path.resolve(__dirname, '../../auto-builder/dist/services/CredentialManager.js');
})(),
];
for (const modPath of candidates) {
try {
// dynamic import handles both ESM & CJS
const m = await Promise.resolve(`${modPath}`).then(s => __importStar(require(s)));
const CredentialManager = m.CredentialManager || (m.default?.CredentialManager);
if (!CredentialManager)
continue;
const manager = CredentialManager.getInstance();
const cred = await manager.getCredentials(credentialId);
if (cred)
return cred;
}
catch {
// try next candidate
}
}
return {};
}
handleContinueOnFail(error, node) {
if (node.continueOnFail) {
return [
{
json: { error: error.message, timestamp: new Date().toISOString() },
binary: {},
pairedItem: { item: 0 },
},
];
}
throw error;
}
/**
* Get the position of this node among nodes of the same type in the workflow
*/
getWorkflowNodePosition(node, context) {
const sameTypeNodes = context.workflow?.nodes?.filter((n) => n.type === node.type) || [];
return sameTypeNodes.findIndex((n) => n.id === node.id) + 1; // 1-based index
}
/**
* Generate a unique prefix for this node based on its type and position in the workflow
* This automatically handles cases where the same node type appears multiple times
*/
generateNodePrefix(node, context) {
const nodePosition = this.getWorkflowNodePosition(node, context);
const cleanType = node.type.replace(/\./g, '_');
return `${cleanType}_${nodePosition}`;
}
/**
* Create a namespaced result object (industry standard approach)
* Instead of prefixing every key, we namespace the entire result under the node type
*/
createNamespacedResult(result, nodeType, nodePosition) {
const cleanType = nodeType.replace(/\./g, '_');
const namespaceKey = nodePosition > 1 ? `${cleanType}_${nodePosition}` : cleanType;
return {
[namespaceKey]: result
};
}
}
exports.BaseNodeExecutor = BaseNodeExecutor;
/**
* Get the position of a node among nodes of the same type in the workflow
*/
const getWorkflowNodePosition = (node, context) => {
const sameTypeNodes = context.workflow?.nodes?.filter((n) => n.type === node.type) || [];
const position = sameTypeNodes.findIndex((n) => n.id === node.id) + 1; // 1-based index
return position > 0 ? position : 1; // Default to 1 if not found
};
exports.getWorkflowNodePosition = getWorkflowNodePosition;
/**
* Get stable output key for a node
* Priority:
* 1. Use node.outputKey if already saved (stable - persisted from previous execution)
* 2. Generate based on position, ensuring no collision with existing saved keys
*
* Returns both the key and whether it's newly generated.
* When isNewlyGenerated is true, the workflow engine should save outputKey to node config.
*/
const getStableOutputKey = (node, context) => {
const nodePosition = (0, exports.getWorkflowNodePosition)(node, context);
// If node already has a saved outputKey, use it (stable)
if (node.outputKey && typeof node.outputKey === 'string') {
return {
outputKey: node.outputKey,
isNewlyGenerated: false,
workflowNodePosition: nodePosition
};
}
const cleanType = node.type.replace(/\./g, '_');
// Collect all existing outputKeys from other nodes of the same type
// This prevents collisions when nodes are reordered or removed
const existingOutputKeys = new Set();
const sameTypeNodes = context.workflow?.nodes?.filter((n) => n.type === node.type) || [];
for (const n of sameTypeNodes) {
if (n.id !== node.id && n.outputKey && typeof n.outputKey === 'string') {
existingOutputKeys.add(n.outputKey);
}
}
// Generate candidate key based on position
let candidateKey = nodePosition > 1 ? `${cleanType}_${nodePosition}` : cleanType;
// If the candidate is already taken by another node's saved key, find next available
if (existingOutputKeys.has(candidateKey)) {
let counter = 1;
while (counter < 1000) { // Safety limit
const testKey = counter === 1 ? cleanType : `${cleanType}_${counter}`;
if (!existingOutputKeys.has(testKey)) {
candidateKey = testKey;
break;
}
counter++;
}
}
return {
outputKey: candidateKey,
isNewlyGenerated: true,
workflowNodePosition: nodePosition
};
};
exports.getStableOutputKey = getStableOutputKey;
/**
* Create a namespaced result object
* Uses the provided outputKey as the namespace key
* @param result - The result data
* @param outputKey - The namespace key to use (from getStableOutputKey)
*/
const createNamespacedResult = (result, outputKey) => {
return {
[outputKey]: result
};
};
exports.createNamespacedResult = createNamespacedResult;
/**
* Create a formatted result that merges input data with namespaced node results
* This prevents key conflicts while preserving all data from previous nodes
* Automatically determines the namespace based on node type and workflow position
*
* @param inputData - The input data from previous nodes
* @param result - The result data from this node's operation
* @param context - The execution context (contains node and workflow info)
* @param dataItemIndex - The index of the current data item being processed (0, 1, 2...)
* @param options - Optional configuration for output format
* @returns Formatted result object with namespaced keys
*/
const createFormattedResult = (inputData, nodeResult, context, dataItemIndex, options = {}) => {
const { legacyMode = false, includeBothFormats = false } = options;
const result = {
...nodeResult,
internal_node_reference: context.node.id
};
// Get stable output key (uses saved key if available, otherwise generates)
const { outputKey, isNewlyGenerated, workflowNodePosition } = (0, exports.getStableOutputKey)(context.node, context);
// Legacy mode: use old behavior (no namespacing) - matches original exactly
if (legacyMode) {
return {
json: {
...inputData.json,
...result,
_metadata: {
operation: context.node.parameters?.operation,
timestamp: new Date().toISOString(),
nodeId: context.node.id,
index: dataItemIndex, // Use 'index' to match original
outputKey, // Include for workflow engine to save
isNewlyGenerated // true = workflow engine should save outputKey to node config
}
},
binary: inputData.binary || {},
pairedItem: { item: dataItemIndex }
};
}
// New namespaced approach (industry standard) - using stable output key
const namespacedResult = (0, exports.createNamespacedResult)({
...result,
_metadata: {
operation: context.node.parameters?.operation,
timestamp: new Date().toISOString(),
nodeId: context.node.id,
nodeType: context.node.type,
dataItemIndex,
workflowNodePosition,
outputKey, // The stable namespace key used for this node
isNewlyGenerated // true = workflow engine should save outputKey to node config
}
}, outputKey);
const namespacedOutput = {
json: {
// Preserve all input data
...inputData.json,
// Add namespaced result data
...namespacedResult
},
binary: inputData.binary || {},
pairedItem: { item: dataItemIndex }
};
// Include both formats for gradual migration
if (includeBothFormats) {
namespacedOutput.json = {
...namespacedOutput.json,
// Also include unprefixed keys for backward compatibility - match original format
...result,
_metadata: {
operation: context.node.parameters?.operation,
timestamp: new Date().toISOString(),
nodeId: context.node.id,
index: dataItemIndex, // Use 'index' to match original
// Additional fields for enhanced functionality
nodeType: context.node.type,
workflowNodePosition,
outputKey,
isNewlyGenerated,
_notice: 'Legacy keys are deprecated. Use namespaced keys instead.'
}
};
}
return namespacedOutput;
};
exports.createFormattedResult = createFormattedResult;
/**
* Create a formatted error result that merges input data with namespaced error information
* Automatically determines the namespace based on node type and workflow position
*
* @param inputData - The input data from previous nodes
* @param error - The error that occurred
* @param context - The execution context (contains node and workflow info)
* @param dataItemIndex - The index of the current data item being processed (0, 1, 2...)
* @param options - Optional configuration for output format
* @returns Formatted error result with namespaced keys
*/
const createFormattedErrorResult = (inputData, error, context, dataItemIndex, options = {}) => {
const { legacyMode = false, includeBothFormats = false } = options;
// Get stable output key (uses saved key if available, otherwise generates)
const { outputKey, isNewlyGenerated, workflowNodePosition } = (0, exports.getStableOutputKey)(context.node, context);
// Legacy mode: use old behavior (no namespacing) - matches original exactly
if (legacyMode) {
return {
json: {
...inputData.json,
error: error.message,
internal_node_reference: context.node.id,
_metadata: {
operation: context.node.parameters?.operation,
failed: true,
timestamp: new Date().toISOString(),
nodeId: context.node.id,
index: dataItemIndex, // Use 'index' to match original
outputKey, // Include for workflow engine to save
isNewlyGenerated // true = workflow engine should save outputKey to node config
}
},
binary: inputData.binary || {},
pairedItem: { item: dataItemIndex }
};
}
// New namespaced approach (industry standard) - using stable output key
const namespacedResult = (0, exports.createNamespacedResult)({
error: error.message,
success: false,
internal_node_reference: context.node.id,
_metadata: {
operation: context.node.parameters?.operation,
failed: true,
timestamp: new Date().toISOString(),
nodeId: context.node.id,
nodeType: context.node.type,
dataItemIndex,
workflowNodePosition,
outputKey, // The stable namespace key used for this node
isNewlyGenerated // true = workflow engine should save outputKey to node config
}
}, outputKey);
const namespacedOutput = {
json: {
// Preserve all input data
...inputData.json,
// Add namespaced error data
...namespacedResult
},
binary: inputData.binary || {},
pairedItem: { item: dataItemIndex }
};
// Include both formats for gradual migration
if (includeBothFormats) {
namespacedOutput.json = {
...namespacedOutput.json,
// Also include unprefixed keys for backward compatibility - match original format
error: error.message,
internal_node_reference: context.node.id,
_metadata: {
operation: context.node.parameters?.operation,
failed: true,
timestamp: new Date().toISOString(),
nodeId: context.node.id,
index: dataItemIndex, // Use 'index' to match original
// Additional fields for enhanced functionality
nodeType: context.node.type,
workflowNodePosition,
outputKey,
isNewlyGenerated,
_notice: 'Legacy keys are deprecated. Use namespaced keys instead.'
}
};
}
return namespacedOutput;
};
exports.createFormattedErrorResult = createFormattedErrorResult;
const validatePlugin = (meta) => meta;
exports.validatePlugin = validatePlugin;
const definePlugin = (meta) => (0, exports.validatePlugin)(meta);
exports.definePlugin = definePlugin;
const registerCredential = (def) => {
if (!def?.name)
throw new Error('Credential definition missing name');
credentialRegistry[def.name] = def;
};
exports.registerCredential = registerCredential;
const getCredentialDef = (name) => credentialRegistry[name];
exports.getCredentialDef = getCredentialDef;
const listCredentialDefinitions = () => Object.values(credentialRegistry);
exports.listCredentialDefinitions = listCredentialDefinitions;
// ============================================================================
// Temp File Management Utilities (Shared across all nodes)
// ============================================================================
/**
* Creates a temporary file with metadata tracking for cleanup
* Returns a reference object with file path and metadata
*
* @param buffer - Binary data to store
* @param workflowId - Workflow ID for tracking
* @param nodeId - Node ID for tracking
* @param fileName - Original filename for extension preservation
* @param metadata - Additional metadata (optional)
* @returns Promise<TempFileReference> - Reference object with file path and metadata
*/
const createTempFile = async (buffer, workflowId, nodeId, fileName, metadata = {}) => {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
const os = await Promise.resolve().then(() => __importStar(require('os')));
// Create consistent temp directory structure: /tmp/auto-builder-temp/
const baseTempDir = path.join(os.tmpdir(), 'auto-builder-temp');
try {
await fs.mkdir(baseTempDir, { recursive: true });
}
catch (error) {
// Directory might already exist, ignore error
}
// Create unique subdirectory for this file
const uniqueDir = await fs.mkdtemp(path.join(baseTempDir, 'file-'));
// Preserve file extension and sanitize filename
const timestamp = Date.now();
const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
const tempFileName = `${timestamp}-${sanitizedFileName}`;
const tempFilePath = path.join(uniqueDir, tempFileName);
// Write binary data
await fs.writeFile(tempFilePath, buffer);
// Create metadata file alongside the temp file
const metadataPath = `${tempFilePath}.meta.json`;
const fileMetadata = {
workflowId,
nodeId,
originalFileName: fileName,
fileSize: buffer.length,
createdAt: new Date().toISOString(),
tempFilePath,
tempDir: uniqueDir,
...metadata
};
await fs.writeFile(metadataPath, JSON.stringify(fileMetadata, null, 2));
exports.log.debug('๐ [TEMP_FILE] Created temp file with metadata', {
tempFilePath,
fileSize: buffer.length,
workflowId,
nodeId,
fileName
});
return {
tempFilePath,
tempDir: uniqueDir,
metadataPath,
reference: {
path: tempFilePath,
size: buffer.length,
originalFileName: fileName,
createdAt: fileMetadata.createdAt,
workflowId,
nodeId
}
};
};
exports.createTempFile = createTempFile;
/**
* Reads a temporary file and returns the buffer
*
* @param tempFilePath - Path to the temporary file (string)
* @returns Promise<Buffer> - Binary data
*/
const readTempFile = async (tempFilePath) => {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
if (!tempFilePath || typeof tempFilePath !== 'string') {
throw new Error('Invalid temp file path: string path is required');
}
try {
const buffer = await fs.readFile(tempFilePath);
exports.log.debug('โ
[TEMP_FILE] Read temp file', { path: tempFilePath, size: buffer.length });
return buffer;
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
exports.log.error('โ [TEMP_FILE] Failed to read temp file', { path: tempFilePath, error: errorMsg });
throw new Error(`Failed to read temp file '${tempFilePath}': ${errorMsg}`);
}
};
exports.readTempFile = readTempFile;
/**
* Deletes a temporary file and its metadata
*
* @param tempFilePath - Path to the temporary file (string)
* @returns Promise<void>
*/
const deleteTempFile = async (tempFilePath) => {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
if (!tempFilePath || typeof tempFilePath !== 'string') {
throw new Error('Invalid temp file path: string path is required');
}
try {
// Delete metadata file if exists
try {
await fs.unlink(`${tempFilePath}.meta.json`);
}
catch { /* ignore */ }
// Delete the actual file
await fs.unlink(tempFilePath);
// Try to delete the parent directory if empty
try {
await fs.rmdir(path.dirname(tempFilePath));
}
catch { /* ignore */ }
exports.log.debug('๐งน [TEMP_FILE] Deleted temp file', { path: tempFilePath });
}
catch (error) {
exports.log.warn('โ ๏ธ [TEMP_FILE] Failed to delete temp file', {
path: tempFilePath,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
};
exports.deleteTempFile = deleteTempFile;
/**
* Gets metadata for a temporary file
*
* @param tempFilePath - Path to the temporary file (string)
* @returns Promise<object | null> - Metadata object or null if not found
*/
const getTempFileMetadata = async (tempFilePath) => {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
if (!tempFilePath || typeof tempFilePath !== 'string') {
return null;
}
try {
const content = await fs.readFile(`${tempFilePath}.meta.json`, 'utf-8');
return JSON.parse(content);
}
catch {
return null;
}
};
exports.getTempFileMetadata = getTempFileMetadata;