/** * @node ocr-upload * @name OCR Upload * @category ocr * @version 1.3.0 * @description Upload a file for OCR processing via pdftoimageapi * @icon file-scan */ const configSchema = { type: 'object', properties: { fileSource: { type: 'string', title: 'File Source', description: 'Path to file data (e.g., {{item.attachments[0]}} for loop iterations, or {{data.attachments[0]}} for direct input). Leave empty to auto-detect from input.' }, baseUrl: { type: 'string', title: 'API Base URL', default: 'http://localhost:3000', description: 'Base URL of the OCR API' }, credentialId: { type: 'string', title: 'Credential', description: 'Bearer or API Key credential for authentication', dynamicOptions: { source: 'credentials', filter: { type: 'api_key' } } }, processingMode: { type: 'string', title: 'Processing Mode', enum: ['auto', 'single', 'batch', 'smart'], default: 'auto', description: 'How to process the uploaded file' }, batchSize: { type: 'number', title: 'Batch Size', default: 5, description: 'Number of pages per batch (for batch mode)' }, targetWidth: { type: 'number', title: 'Target Width', description: 'Target width in pixels for image conversion' }, groupId: { type: 'string', title: 'Group ID', description: 'Optional group ID to associate jobs' } }, required: ['baseUrl'] }; const inputSchema = { type: 'object', properties: { file: { type: 'object', description: 'Binary file object with type, data (base64), mimeType, filename, size' }, storage: { type: 'object', description: 'Storage reference to retrieve file from database', properties: { collection: { type: 'string' }, id: { type: 'string' } } } } }; const outputSchema = { type: 'object', properties: { job: { type: 'object', properties: { id: { type: 'string' }, status: { type: 'string' }, filename: { type: 'string' }, created_at: { type: 'string' } } } } }; const outputs = [ { name: 'main', displayName: 'Job Created', type: 'object', color: '#10b981' } ]; function isBinaryInput(input) { // Binary input can have either data (base64) or filePath (disk reference) return input && input.type === 'binary' && (typeof input.data === 'string' || typeof input.filePath === 'string'); } function resolveBinaryData(file) { // If file has filePath, read from disk if (file.filePath && !file.data) { const readResult = smartbotic.fs.readFile(file.filePath, 'base64'); if (readResult.success) { file.data = readResult.data; smartbotic.log.info(`Loaded binary from disk: ${file.filePath}`); } else { throw new Error(`Failed to read file from disk: ${file.filePath} - ${readResult.error}`); } } return file; } module.exports = { configSchema, inputSchema, outputSchema, outputs, async execute(config, input, context) { const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, ''); let file = null; // First priority: use configured fileSource if provided // The expression (e.g., {{item.attachments[0]}}) is evaluated by the workflow engine, // so config.fileSource contains the actual file object if (config.fileSource && typeof config.fileSource === 'object') { if (isBinaryInput(config.fileSource)) { file = config.fileSource; smartbotic.log.info('OCR Upload: Using configured file source'); } else if (Array.isArray(config.fileSource) && config.fileSource.length > 0) { // If fileSource resolved to an array (e.g., attachments array), use first item file = config.fileSource[0]; smartbotic.log.info(`OCR Upload: Using first item from configured file source array: ${file.filename}`); } } // Fallback: auto-detect from various input formats if (!file) { if (isBinaryInput(input.file)) { file = input.file; } else if (isBinaryInput(input)) { file = input; } else if (input.data && isBinaryInput(input.data)) { // Handle data wrapper from upstream nodes file = input.data; } else if (input.attachments && Array.isArray(input.attachments) && input.attachments.length > 0) { // Handle output from IMAP Extract Attachments - use first attachment file = input.attachments[0]; smartbotic.log.info(`Using first attachment from array: ${file.filename}`); } else if (input.storage && input.storage.collection && input.storage.id) { // Load from storage database (metadata with filePath) const storageResult = smartbotic.storage.get(input.storage.collection, input.storage.id); if (!storageResult.found) { throw new Error(`File not found in storage: ${input.storage.collection}/${input.storage.id}`); } const doc = storageResult.document; file = { type: 'binary', filePath: doc.filePath, // Reference to disk location mimeType: doc.mimeType, filename: doc.filename, size: doc.size }; } } if (!file) { // Log available input keys for debugging const inputKeys = Object.keys(input || {}).join(', '); smartbotic.log.warn(`OCR Upload: No binary file found. Input keys: ${inputKeys}`); throw new Error('No file provided. Configure the "File Source" field with an expression like {{item.attachments[0]}} for loop iterations, or connect to IMAP Extract Attachments.'); } // Resolve file data from disk if needed file = resolveBinaryData(file); const headers = {}; if (config.credentialId) { const auth = smartbotic.credentials.get(config.credentialId); if (!auth.success) { throw new Error(`Failed to load credential: ${auth.error}`); } headers[auth.headerName] = auth.headerValue; } smartbotic.log.info(`OCR Upload: ${file.filename} (${file.size} bytes, type: ${file.mimeType})`); // Build form data for additional parameters const formData = {}; if (config.processingMode && config.processingMode !== 'auto') { formData.processing_mode = config.processingMode; } if (config.batchSize) { formData.batch_size = String(config.batchSize); } if (config.targetWidth) { formData.target_width = String(config.targetWidth); } if (config.groupId) { formData.group_id = config.groupId; } // Use the new multipart file upload support with proper binary encoding const response = smartbotic.http.request({ method: 'POST', url: `${baseUrl}/api/ocr/upload`, headers: headers, files: [{ name: 'file', filename: file.filename, mimeType: file.mimeType, data: file.data // base64 encoded - will be decoded to binary by http.request }], formData: formData, timeout: 120000 }); if (response.status < 200 || response.status >= 300) { const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data); if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`); } const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; return { job: data.job || data }; } };