WFDASH-WEBAPP/n8n_workfows/SNIPPET-plan-upload-attachments-early-v3.js
SJCoetz 33591959c2 feat: add quote email intake and atomic numbering
This commit implements the full inbound quote email processing workflow and atomic quote numbering:
- Add PocketBase collections for monthly atomic quote counters, email attachments, and threaded replies
- Add idempotent migration script to set up new collections and add deduplication field to existing quotes table
- Implement `/api/v1/quotes/next-number` endpoint for properly formatted Q-YYYY-MM-NNN quote numbers
- Add MCP tools for idempotent quote creation, reply threading, and message ID lookup
- Add end-to-end and concurrent smoke tests for pipeline validation
- Update TypeScript types for new quote-related entities
- Update project documentation and package.json scripts
- Mark quote email intake as complete in the parity backlog
- Add n8n v2 workflows for automated email monitoring and error alerts
2026-06-22 13:58:24 +02:00

118 lines
3.9 KiB
JavaScript

// 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;