UNPKG

n8n

Version:

n8n Workflow Automation Tool

316 lines 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyProviderShapeNormalizers = applyProviderShapeNormalizers; exports.findProviderShapeViolation = findProviderShapeViolation; const n8n_core_1 = require("n8n-core"); let canvasPngB64; function placeholderImageB64() { canvasPngB64 ??= (0, n8n_core_1.evalCanvasPng)().toString('base64'); return canvasPngB64; } const DEFAULT_HUBSPOT_VID = 3234574; function isPlainObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } function nowUnix() { return Math.floor(Date.now() / 1000); } function isOpenAiImagesGeneration(path) { return path.endsWith('/images/generations'); } function isOpenAiCompletion(path) { if (/\/v\d+\/responses\/?$/.test(path)) return 'responses'; if (/\/chat\/completions\/?$/.test(path)) return 'chat'; return null; } function isGeminiGenerate(host, path) { return host === 'generativelanguage.googleapis.com' && path.includes('generatecontent'); } function isRedditWrite(host, path, method) { if (method.toUpperCase() !== 'POST' || !host.includes('reddit.com')) return null; if (path.includes('api/comment')) return 'comment'; if (path.includes('api/submit')) return 'submit'; return null; } function isHubspotUpsert(host, path) { return host.includes('hubapi.com') && path.includes('/contacts/v1/contact/createorupdate'); } function isGoogleDocsBatchUpdate(host, path) { return (host === 'docs.googleapis.com' && path.includes('/documents/') && path.includes(':batchupdate')); } function isGmailMessagesList(host, path, method) { return (method.toUpperCase() === 'GET' && host.endsWith('googleapis.com') && /\/gmail\/v1\/users\/[^/]+\/messages\/?$/.test(path)); } function extractAnswerText(body) { if (body === null || body === undefined) return ''; if (typeof body === 'string') return body; if (!isPlainObject(body)) return String(body); if (typeof body.text === 'string') return body.text; if (typeof body.content === 'string') return body.content; if (typeof body.output_text === 'string') return body.output_text; if (typeof body.message === 'string') return body.message; return JSON.stringify(body); } function normalizeOpenAiImages(spec) { const body = spec.body; let base = {}; let rawEntries; if (isPlainObject(body) && Array.isArray(body.data)) { base = body; rawEntries = body.data; } else if (Array.isArray(body)) { rawEntries = body; } else if (isPlainObject(body) && ('b64_json' in body || 'url' in body)) { rawEntries = [body]; } else if (isPlainObject(body)) { base = body; } const entries = rawEntries && rawEntries.length > 0 ? rawEntries : [{}]; const data = entries.map(coerceImageEntry); spec.body = { created: nowUnix(), ...base, data }; } function coerceImageEntry(entry) { const base = isPlainObject(entry) ? entry : {}; return { ...base, b64_json: placeholderImageB64() }; } function normalizeOpenAiCompletionText(spec, kind) { const body = spec.body; if (!isPlainObject(body)) return; if (kind === 'responses') { if (Array.isArray(body.output)) { for (const item of body.output) { if (!isPlainObject(item) || !Array.isArray(item.content)) continue; for (const part of item.content) { if (isPlainObject(part) && 'text' in part && typeof part.text !== 'string') { part.text = JSON.stringify(part.text); } } } } if (body.output_text !== undefined && typeof body.output_text !== 'string') { body.output_text = JSON.stringify(body.output_text); } return; } if (Array.isArray(body.choices)) { for (const choice of body.choices) { if (!isPlainObject(choice) || !isPlainObject(choice.message)) continue; const content = choice.message.content; if (content !== null && content !== undefined && typeof content !== 'string') { if (!Array.isArray(content)) choice.message.content = JSON.stringify(content); } } } } function normalizeGemini(spec) { if (hasGeminiCandidates(spec.body)) return; const text = extractAnswerText(spec.body); spec.body = { candidates: [{ content: { parts: [{ text }], role: 'model' }, finishReason: 'STOP', index: 0 }], usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0, totalTokenCount: 0 }, }; } function hasGeminiCandidates(body) { if (!isPlainObject(body) || !Array.isArray(body.candidates) || body.candidates.length === 0) { return false; } const first = body.candidates[0]; if (!isPlainObject(first) || !isPlainObject(first.content)) return false; return Array.isArray(first.content.parts) && first.content.parts.length > 0; } function normalizeReddit(spec, kind) { const inner = extractRedditData(spec.body); const data = kind === 'comment' ? ensureRedditThings(inner) : inner; spec.body = { json: { errors: existingRedditErrors(spec.body), data } }; } function extractRedditData(body) { if (isPlainObject(body) && isPlainObject(body.json) && isPlainObject(body.json.data)) { return body.json.data; } if (isPlainObject(body) && isPlainObject(body.data)) return body.data; if (isPlainObject(body)) return body; return {}; } function existingRedditErrors(body) { if (isPlainObject(body) && isPlainObject(body.json) && Array.isArray(body.json.errors)) { return body.json.errors; } return []; } function ensureRedditThings(data) { if (Array.isArray(data.things) && isPlainObject(data.things[0]) && data.things[0].data) { return data; } return { things: [{ kind: 't1', data }] }; } function normalizeHubspotUpsert(spec) { const body = isPlainObject(spec.body) ? spec.body : {}; spec.body = { ...body, vid: resolveHubspotVid(body), isNew: typeof body.isNew === 'boolean' ? body.isNew : true, }; } function resolveHubspotVid(body) { for (const candidate of [body.vid, body['canonical-vid'], body.id]) { if (typeof candidate === 'number' && Number.isFinite(candidate)) return candidate; if (typeof candidate === 'string' && /^\d+$/.test(candidate)) return Number(candidate); } return DEFAULT_HUBSPOT_VID; } function normalizeGmailMessagesList(spec) { if (!Array.isArray(spec.body)) return; spec.body = { messages: spec.body, resultSizeEstimate: spec.body.length }; } function normalizeGoogleDocsBatchUpdate(spec) { const body = isPlainObject(spec.body) ? spec.body : {}; const replies = Array.isArray(body.replies) && body.replies.length > 0 ? body.replies : [{}]; spec.body = { ...body, replies }; } function applyProviderShapeNormalizers(info, spec) { if (spec.type !== 'json') return; const path = info.pathname.toLowerCase(); const host = (info.hostname ?? '').toLowerCase(); if (isOpenAiImagesGeneration(path)) return normalizeOpenAiImages(spec); const completionKind = isOpenAiCompletion(path); if (completionKind) return normalizeOpenAiCompletionText(spec, completionKind); if (isGeminiGenerate(host, path)) return normalizeGemini(spec); const redditKind = isRedditWrite(host, path, info.method); if (redditKind) return normalizeReddit(spec, redditKind); if (isHubspotUpsert(host, path)) return normalizeHubspotUpsert(spec); if (isGoogleDocsBatchUpdate(host, path)) return normalizeGoogleDocsBatchUpdate(spec); if (isGmailMessagesList(host, path, info.method)) return normalizeGmailMessagesList(spec); } function findProviderShapeViolation(info, body) { const path = info.pathname.toLowerCase(); const host = (info.hostname ?? '').toLowerCase(); if (isOpenAiImagesGeneration(path)) return openAiImagesViolation(body); const completionKind = isOpenAiCompletion(path); if (completionKind) return openAiCompletionTextViolation(body, completionKind); if (isGeminiGenerate(host, path)) return geminiViolation(body); const redditKind = isRedditWrite(host, path, info.method); if (redditKind) return redditViolation(body, redditKind); if (isHubspotUpsert(host, path)) return hubspotViolation(body); if (isGoogleDocsBatchUpdate(host, path)) return googleDocsViolation(body); if (isGmailMessagesList(host, path, info.method)) return gmailMessagesListViolation(body); return undefined; } function gmailMessagesListViolation(body) { if (!Array.isArray(body)) return undefined; return 'Invalid: Gmail messages.list returns an OBJECT envelope `{ "messages": [{ "id", "threadId" }, ...], "resultSizeEstimate": <n> }` — the node reads response.messages, so a bare array yields zero messages. Resubmit wrapped in that envelope.'; } function openAiImagesViolation(body) { const data = isPlainObject(body) ? body.data : undefined; if (!Array.isArray(data) || data.length === 0) { return 'Invalid: OpenAI /v1/images/generations must return a JSON object with a `data` ARRAY of image objects, e.g. `{ "created": <unix>, "data": [{ "b64_json": "..." }] }`. Resubmit with that envelope.'; } const brokenEntry = data.some((entry) => !isPlainObject(entry) || (typeof entry.b64_json !== 'string' && typeof entry.url !== 'string')); if (brokenEntry) { return 'Invalid: every OpenAI image `data[]` entry must carry a `b64_json` (base64 string) — the node decodes it. Resubmit with `data: [{ "b64_json": "..." }]`.'; } return undefined; } function openAiCompletionTextViolation(body, kind) { if (!isPlainObject(body)) return undefined; if (kind === 'responses' && Array.isArray(body.output)) { for (const item of body.output) { if (!isPlainObject(item) || !Array.isArray(item.content)) continue; for (const part of item.content) { if (isPlainObject(part) && 'text' in part && typeof part.text !== 'string') { return 'Invalid: OpenAI /v1/responses `output[].content[].text` must be a STRING — when the answer is JSON, JSON-encode it into the string (e.g. "{\\"quotes\\":[...]}"), never embed the parsed object. Resubmit with `text` as a string.'; } } } } if (kind === 'chat' && Array.isArray(body.choices)) { for (const choice of body.choices) { if (!isPlainObject(choice) || !isPlainObject(choice.message)) continue; const content = choice.message.content; if (content !== null && content !== undefined && typeof content !== 'string') { if (!Array.isArray(content)) { return 'Invalid: chat/completions `choices[].message.content` must be a STRING (or null with tool_calls) — when the answer is JSON, JSON-encode it into the string, never embed the parsed object. Resubmit with `content` as a string.'; } } } } return undefined; } function geminiViolation(body) { if (hasGeminiCandidates(body)) return undefined; return 'Invalid: Google Gemini responses must use the full envelope `{ "candidates": [{ "content": { "parts": [{ "text": "..." }] } }] }` — the node reads candidates[].content.parts[].text. Resubmit wrapped in that shape.'; } function redditViolation(body, kind) { const data = isPlainObject(body) && isPlainObject(body.json) ? body.json.data : undefined; if (!isPlainObject(data)) { return 'Invalid: Reddit write responses must use the `{ "json": { "errors": [], "data": { ... } } }` envelope — the node reads response.json.data. Resubmit wrapped in that shape.'; } if (kind === 'comment') { const things = data.things; if (!Array.isArray(things) || !isPlainObject(things[0]) || !things[0].data) { return 'Invalid: Reddit api/comment responses put the comment under `json.data.things[0].data`. Resubmit with `{ "json": { "data": { "things": [{ "kind": "t1", "data": { ... } }] } } }`.'; } } return undefined; } function hubspotViolation(body) { const vid = isPlainObject(body) ? body.vid : undefined; if (typeof vid === 'number' || (typeof vid === 'string' && /^\d+$/.test(vid))) return undefined; return 'Invalid: HubSpot contacts/v1 createOrUpdate returns `{ "vid": <number>, "isNew": <bool> }` — the node reads response.vid. Resubmit with a numeric `vid`.'; } function googleDocsViolation(body) { const replies = isPlainObject(body) ? body.replies : undefined; if (Array.isArray(replies) && replies.length > 0) return undefined; return 'Invalid: Google Docs batchUpdate returns `{ "documentId": "...", "replies": [ ... ] }` with one reply per request — the node reads response.replies[0]. Resubmit with a non-empty `replies` array.'; } //# sourceMappingURL=provider-shapes.js.map