WFDASH-WEBAPP/n8n_workfows/SNIPPET-plan-upload-attachments-early-v2.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

116 lines
4.1 KiB
JavaScript

// Plan: Upload Attachments Early v2 — does PB admin auth INLINE so binary survives.
// (v1 relied on a pre-computed $json.pb_token from a separate PocketBase
// Admin Auth HTTP node — but that node strips binary, so v1's uploads had
// no bytes to send. v2 inlines the auth via this.helpers.httpRequest.)
//
// On each invocation:
// 1. Read PB admin creds from PocketBase Connection (the Set node upstream)
// 2. POST to /api/collections/_superusers/auth-with-password → get token
// 3. Walk the binary on the incoming item(s), emit one item per attachment
// with the freshly-minted token + all the upload prep data
// 4. Items carry the binary so the next HTTP node can multipart-upload it
//
// On no-attachments emails, emits one placeholder item with no binary.
const inputs = $input.all();
if (inputs.length === 0) {
return [{ json: { _attachment_count: 0 }, binary: {} }];
}
// 1. Read PB admin creds from PocketBase Connection (the Set node upstream).
// We use $('Node Name').item.json so we don't depend on what $json carries
// (the HTTP node that previously did the auth replaced $json with its
// own response shape).
let ctx = {};
try {
ctx = $('PocketBase Connection').item.json || {};
} catch (e) {
ctx = inputs[0].json || {};
}
const pbUrl = String(ctx.pb_url || '');
const pbEmail = String(ctx.pb_admin_email || '');
const pbPassword = String(ctx.pb_admin_password || '');
const messageId = String(ctx.message_id || '');
const effectiveMessageId = String(ctx._effective_message_id || messageId);
if (!pbUrl || !pbEmail || !pbPassword) {
throw new Error('PB credentials missing from PocketBase Connection: ' + JSON.stringify({
hasUrl: !!pbUrl, hasEmail: !!pbEmail, hasPass: !!pbPassword
}));
}
// 2. POST to PB to mint an admin token. This happens INSIDE the Code node,
// so binary on the input items is preserved (unlike going through an HTTP
// Request node, which strips binary).
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));
}
// 3. Walk binary on every input item and emit one upload-prep item per
// attachment. The binary is re-anchored under a stable `attachment`
// property so the next HTTP node can reference it predictably.
const out = [];
let attachmentIndex = 0;
for (const input of inputs) {
const bin = input.binary || {};
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: {
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: { attachment: b },
});
attachmentIndex++;
}
}
if (out.length === 0) {
// No attachments — emit a placeholder item with no binary so the chain
// continues. The downstream HTTP POST will see no binary and PB will
// reject (caught by onError: continueRegularOutput on the HTTP node).
out.push({
json: {
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;