/** * @node ocr-upload * @name OCR Upload * @category ocr * @version 1.0.1 * @description Upload a file for OCR processing via pdftoimageapi * @icon file-scan */ const configSchema = { type: 'object', properties: { 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) { return input && input.type === 'binary' && typeof input.data === 'string'; } function generateBoundary() { return '----SmartBoticBoundary' + smartbotic.utils.uuid().replace(/-/g, ''); } function buildMultipartBody(boundary, file, options) { const crlf = '\r\n'; let body = ''; body += `--${boundary}${crlf}`; body += `Content-Disposition: form-data; name="file"; filename="${file.filename}"${crlf}`; body += `Content-Type: ${file.mimeType}${crlf}`; body += `Content-Transfer-Encoding: base64${crlf}`; body += crlf; body += file.data; body += crlf; if (options.processingMode && options.processingMode !== 'auto') { body += `--${boundary}${crlf}`; body += `Content-Disposition: form-data; name="processing_mode"${crlf}`; body += crlf; body += options.processingMode; body += crlf; } if (options.batchSize) { body += `--${boundary}${crlf}`; body += `Content-Disposition: form-data; name="batch_size"${crlf}`; body += crlf; body += String(options.batchSize); body += crlf; } if (options.targetWidth) { body += `--${boundary}${crlf}`; body += `Content-Disposition: form-data; name="target_width"${crlf}`; body += crlf; body += String(options.targetWidth); body += crlf; } if (options.groupId) { body += `--${boundary}${crlf}`; body += `Content-Disposition: form-data; name="group_id"${crlf}`; body += crlf; body += options.groupId; body += crlf; } body += `--${boundary}--${crlf}`; return body; } module.exports = { configSchema, inputSchema, outputSchema, outputs, async execute(config, input, context) { const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, ''); let file = null; if (isBinaryInput(input.file)) { file = input.file; } else if (isBinaryInput(input)) { file = input; } else if (input.storage && input.storage.collection && input.storage.id) { 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', data: doc.data, mimeType: doc.mimeType, filename: doc.filename, size: doc.size }; } if (!file) { throw new Error('No file provided. Provide either a binary file object or a storage reference.'); } 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; } const boundary = generateBoundary(); headers['Content-Type'] = `multipart/form-data; boundary=${boundary}`; const body = buildMultipartBody(boundary, file, { processingMode: config.processingMode, batchSize: config.batchSize, targetWidth: config.targetWidth, groupId: config.groupId }); smartbotic.log.info(`OCR Upload: ${file.filename} (${file.size} bytes)`); const response = smartbotic.http.request({ method: 'POST', url: `${baseUrl}/api/ocr/upload`, headers: headers, body: body, 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 }; } };