// Plan: Upload Attachments Early v3 — minimal change from v2: // - Don't re-anchor binary under 'attachment'; preserve original keys // - Read context from inputs[0].json (the Set node output) AND fallback // to $('PocketBase Connection').item.json // - HTTP node downstream uses {{ $binary[$json.binary_key] }} like v4 // // Why v2 broke: re-anchoring binary under a new key plus the HTTP node's // multipart file param `$binary.attachment` triggered a n8n streams error // ("source.on is not a function"). Sticking to the original key (which // matches the input binary's keys, e.g. "attachment_0") makes the HTTP // node happy. const inputs = $input.all(); if (inputs.length === 0) { return [{ json: { _attachment_count: 0 }, binary: {} }]; } // Read PB admin creds from PocketBase Connection (Set node upstream) so // we're not at the mercy of $json shape after the Set node runs. let pbCtx = {}; try { pbCtx = $('PocketBase Connection').item.json || {}; } catch (e) { pbCtx = inputs[0].json || {}; } // Also pull email context from the first input item (PocketBase Connection // preserves it via includeOtherFields: true). const emailCtx = inputs[0].json || {}; const pbUrl = String(pbCtx.pb_url || ''); const pbEmail = String(pbCtx.pb_admin_email || ''); const pbPassword = String(pbCtx.pb_admin_password || ''); const messageId = String(emailCtx.message_id || ''); const effectiveMessageId = String(emailCtx._effective_message_id || messageId); if (!pbUrl || !pbEmail || !pbPassword) { throw new Error('PB credentials missing from PocketBase Connection'); } // Mint admin token INSIDE this Code node so binary on inputs is preserved // (an HTTP Request node between here and the source would strip it). let pbToken = ''; let pbAdminId = ''; try { const authResp = await this.helpers.httpRequest({ method: 'POST', url: pbUrl + '/api/collections/_superusers/auth-with-password', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: { identity: pbEmail, password: pbPassword }, json: true, timeout: 15000, }); pbToken = String(authResp.token || ''); pbAdminId = String((authResp.admin && authResp.admin.id) || ''); if (!pbToken) { throw new Error('PB auth returned no token: ' + JSON.stringify(authResp)); } } catch (authErr) { throw new Error('PB admin auth failed: ' + (authErr.message || authErr)); } // Emit one item per attachment. PRESERVE the original binary keys so the // downstream HTTP node can use {{ $binary[$json.binary_key] }} (v4 pattern). const out = []; let attachmentIndex = 0; for (const input of inputs) { const bin = input.binary || {}; // Preserve the original binary blob under each original key const outBin = {}; for (const key of Object.keys(bin)) { outBin[key] = bin[key]; } for (const key of Object.keys(bin)) { const b = bin[key]; if (!b) continue; const filename = String(b.fileName || 'attachment'); const contentType = String(b.mimeType || 'application/octet-stream'); out.push({ json: Object.assign({}, emailCtx, { pb_url: pbUrl, pb_token: pbToken, pb_admin_id: pbAdminId, email_message_id: effectiveMessageId, filename: filename, content_type: contentType, size_bytes: Number(b.fileSize || 0), binary_key: key, _upload_index: attachmentIndex, }), binary: outBin, // same binary blob, original keys }); attachmentIndex++; // Only emit one item per binary blob, not one per key, to avoid dup break; } } if (out.length === 0) { // No attachments — emit a single placeholder with no binary out.push({ json: Object.assign({}, emailCtx, { pb_url: pbUrl, pb_token: pbToken, pb_admin_id: pbAdminId, email_message_id: effectiveMessageId, filename: '', content_type: '', size_bytes: 0, binary_key: '__none__', _upload_index: 0, _no_attachments: true, }), binary: {}, }); } return out;