UNPKG

n8n

Version:

n8n Workflow Automation Tool

498 lines 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.partialUpdateOperationSchema = void 0; exports.applyOperations = applyOperations; exports.toWorkflowSlice = toWorkflowSlice; const n8n_workflow_1 = require("n8n-workflow"); const uuid_1 = require("uuid"); const zod_1 = require("zod"); const positionSchema = () => zod_1.z .array(zod_1.z.number()) .length(2) .transform((v) => [v[0], v[1]]) .describe('Canvas position as [x, y]'); const credentialsSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.object({ id: zod_1.z.string().optional(), name: zod_1.z.string() })); exports.partialUpdateOperationSchema = zod_1.z.discriminatedUnion('type', [ zod_1.z.object({ type: zod_1.z.literal('updateNodeParameters'), nodeName: zod_1.z.string().describe('Name of the existing node to update.'), parameters: zod_1.z .record(zod_1.z.string(), zod_1.z.unknown()) .describe('Parameter object to merge into (or replace) the node parameters.'), replace: zod_1.z .boolean() .optional() .describe('If true, replace the node parameters entirely with `parameters`. If false or omitted, deep-merge `parameters` into the existing parameters.'), }), zod_1.z.object({ type: zod_1.z.literal('setNodeParameter'), nodeName: zod_1.z.string().describe('Name of the existing node to update.'), path: zod_1.z .string() .min(2) .describe('JSON Pointer (RFC 6901) path to the parameter to set, e.g. "/jsonSchema" or "/options/systemMessage". Must start with "/". Intermediate objects are created on demand. Array indices are NOT supported — to change a value inside an array, set the whole array. Use this instead of `updateNodeParameters` when you only need to set one nested key — the payload stays small regardless of the rest of the parameters object.'), value: zod_1.z .unknown() .refine((v) => v !== undefined, { message: 'value is required' }) .describe('Value to set at the path. Any defined JSON value.'), }), zod_1.z.object({ type: zod_1.z.literal('addNode'), node: zod_1.z .object({ name: zod_1.z.string().describe('Unique node name. Must not collide with an existing node.'), type: zod_1.z.string().describe('Fully qualified node type, e.g. "n8n-nodes-base.set".'), typeVersion: zod_1.z.number(), parameters: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(), position: positionSchema().optional(), credentials: credentialsSchema.optional(), disabled: zod_1.z.boolean().optional(), notes: zod_1.z.string().optional(), id: zod_1.z.string().optional().describe('Optional node id. Generated if omitted.'), }) .describe('The node to add to the workflow.'), }), zod_1.z.object({ type: zod_1.z.literal('removeNode'), nodeName: zod_1.z .string() .describe('Name of the node to remove. All inbound and outbound connections are removed, including any sub-node attachments (LLM models, memory, tools) — the sub-nodes themselves remain in the workflow but become disconnected and will not be re-attached automatically. To modify a node, use updateNodeParameters or setNodeParameter instead.'), }), zod_1.z.object({ type: zod_1.z.literal('renameNode'), oldName: zod_1.z.string(), newName: zod_1.z.string().describe('New unique node name.'), }), zod_1.z.object({ type: zod_1.z.literal('addConnection'), source: zod_1.z.string().describe('Name of the source node.'), target: zod_1.z.string().describe('Name of the target node.'), sourceIndex: zod_1.z .number() .int() .nonnegative() .optional() .describe('Source output index. Default 0.'), targetIndex: zod_1.z .number() .int() .nonnegative() .optional() .describe('Target input index. Default 0.'), connectionType: zod_1.z .string() .optional() .describe('Connection type, e.g. "main" or "ai_languageModel". Default "main".'), }), zod_1.z.object({ type: zod_1.z.literal('removeConnection'), source: zod_1.z.string(), target: zod_1.z.string(), sourceIndex: zod_1.z.number().int().nonnegative().optional(), targetIndex: zod_1.z.number().int().nonnegative().optional(), connectionType: zod_1.z.string().optional(), }), zod_1.z.object({ type: zod_1.z.literal('setNodeCredential'), nodeName: zod_1.z.string(), credentialKey: zod_1.z .string() .describe('Credential key on the node, e.g. "slackApi" or "httpHeaderAuth".'), credentialId: zod_1.z.string(), credentialName: zod_1.z.string(), }), zod_1.z.object({ type: zod_1.z.literal('setNodePosition'), nodeName: zod_1.z.string(), position: positionSchema(), }), zod_1.z.object({ type: zod_1.z.literal('setNodeDisabled'), nodeName: zod_1.z.string(), disabled: zod_1.z.boolean(), }), zod_1.z.object({ type: zod_1.z.literal('setNodeSettings'), nodeName: zod_1.z.string().describe('Name of the existing node to update.'), settings: zod_1.z .object({ onError: zod_1.z .enum(['stopWorkflow', 'continueRegularOutput', 'continueErrorOutput']) .optional() .describe('How the node behaves on error. "stopWorkflow" halts the run; "continueRegularOutput" forwards an empty item on the main output; "continueErrorOutput" routes the failure to the node\'s error output. Required for sub-nodes (LLM model, memory, tools) since the canvas UI does not expose this setting for them.'), retryOnFail: zod_1.z.boolean().optional(), maxTries: zod_1.z .number() .int() .min(2) .max(5) .optional() .describe('Number of attempts when retryOnFail is true (2–5).'), waitBetweenTries: zod_1.z .number() .int() .min(0) .max(5000) .optional() .describe('Milliseconds to wait between retry attempts (0–5000).'), alwaysOutputData: zod_1.z.boolean().optional(), executeOnce: zod_1.z.boolean().optional(), }) .refine((s) => Object.keys(s).length > 0, { message: 'settings must specify at least one field', }) .describe('Node-level execution settings. Only the keys you include are written; omitted keys are left unchanged.'), }), zod_1.z.object({ type: zod_1.z.literal('setWorkflowMetadata'), name: zod_1.z.string().max(128).optional(), description: zod_1.z.string().max(255).optional(), }), ]); const cloneWorkflow = (workflow) => ({ name: workflow.name, description: workflow.description, nodes: workflow.nodes.map((node) => structuredClone(node)), connections: structuredClone(workflow.connections), }); const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value); const sanitizeUnsafeKeys = (value) => { if (Array.isArray(value)) return value.map(sanitizeUnsafeKeys); if (!isPlainObject(value)) return value; const out = {}; for (const [key, v] of Object.entries(value)) { if (!(0, n8n_workflow_1.isSafeObjectProperty)(key)) continue; out[key] = sanitizeUnsafeKeys(v); } return out; }; const parseJsonPointer = (path) => { if (!path.startsWith('/')) return null; const tail = path.slice(1); if (tail.length === 0) return null; const rawSegments = tail.split('/'); const segments = []; for (const raw of rawSegments) { if (/~(?:[^01]|$)/.test(raw)) return null; const seg = raw.replace(/~1/g, '/').replace(/~0/g, '~'); if (seg.length === 0 || !(0, n8n_workflow_1.isSafeObjectProperty)(seg)) return null; segments.push(seg); } return segments; }; const setAtPointer = (root, segments, value) => { let cursor = root; for (let i = 0; i < segments.length - 1; i++) { const key = segments[i]; const next = cursor[key]; if (next === undefined) { const child = {}; cursor[key] = child; cursor = child; } else if (isPlainObject(next)) { cursor = next; } else { return `cannot descend into non-object at '/${segments.slice(0, i + 1).join('/')}'`; } } cursor[segments[segments.length - 1]] = sanitizeUnsafeKeys(value); return null; }; const deepMerge = (target, source) => { const result = { ...target }; for (const [key, value] of Object.entries(source)) { if (!(0, n8n_workflow_1.isSafeObjectProperty)(key)) continue; const existing = Object.prototype.hasOwnProperty.call(result, key) ? result[key] : undefined; if (isPlainObject(existing) && isPlainObject(value)) { result[key] = deepMerge(existing, value); } else { result[key] = sanitizeUnsafeKeys(value); } } return result; }; const removeConnectionsFor = (connections, nodeName) => { delete connections[nodeName]; for (const sourceName of Object.keys(connections)) { const byType = connections[sourceName]; for (const connectionType of Object.keys(byType)) { const outputs = byType[connectionType]; for (let i = 0; i < outputs.length; i++) { const targets = outputs[i]; if (!targets) continue; outputs[i] = targets.filter((c) => c.node !== nodeName); } if (outputs.every((o) => !o || o.length === 0)) { delete byType[connectionType]; } } if (Object.keys(byType).length === 0) { delete connections[sourceName]; } } }; const renameInConnections = (connections, oldName, newName) => { if (connections[oldName]) { connections[newName] = connections[oldName]; delete connections[oldName]; } for (const sourceName of Object.keys(connections)) { const byType = connections[sourceName]; for (const connectionType of Object.keys(byType)) { const outputs = byType[connectionType]; for (const targets of outputs) { if (!targets) continue; for (const conn of targets) { if (conn.node === oldName) conn.node = newName; } } } } }; const ensureOutputSlot = (connections, source, connectionType, sourceIndex) => { const byType = (connections[source] ??= {}); const outputs = (byType[connectionType] ??= []); while (outputs.length <= sourceIndex) outputs.push(null); const slot = outputs[sourceIndex] ?? []; outputs[sourceIndex] = slot; return slot; }; const pruneConnectionShape = (connections, source, connectionType) => { const byType = connections[source]; if (!byType) return; const outputs = byType[connectionType]; if (outputs && outputs.every((o) => !o || o.length === 0)) { delete byType[connectionType]; } if (Object.keys(byType).length === 0) { delete connections[source]; } }; const fail = (opIndex, message) => ({ success: false, error: `Operation ${opIndex} failed: ${message}`, opIndex, }); function applyOperations(input, operations) { const workflow = cloneWorkflow(input); const nodeByName = new Map(workflow.nodes.map((n) => [n.name, n])); const addedNodeNames = new Set(); for (let i = 0; i < operations.length; i++) { const op = operations[i]; switch (op.type) { case 'updateNodeParameters': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); const sanitized = sanitizeUnsafeKeys(op.parameters); const merged = op.replace ? sanitized : deepMerge((node.parameters ?? {}), sanitized); node.parameters = merged; break; } case 'setNodeParameter': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); const segments = parseJsonPointer(op.path); if (!segments) { return fail(i, `path '${op.path}' is invalid or contains unsafe segments`); } const params = (node.parameters ?? {}); const setError = setAtPointer(params, segments, op.value); if (setError) return fail(i, setError); node.parameters = params; break; } case 'addNode': { if (!(0, n8n_workflow_1.isSafeObjectProperty)(op.node.name)) { return fail(i, `node name '${op.node.name}' is not allowed`); } if (nodeByName.has(op.node.name)) { return fail(i, `a node named '${op.node.name}' already exists`); } const node = { id: op.node.id ?? (0, uuid_1.v4)(), name: op.node.name, type: op.node.type, typeVersion: op.node.typeVersion, position: op.node.position ?? [0, 0], parameters: (sanitizeUnsafeKeys(op.node.parameters ?? {}) ?? {}), }; if (op.node.credentials) { const credentialEntries = []; for (const [key, cred] of Object.entries(op.node.credentials)) { if (!(0, n8n_workflow_1.isSafeObjectProperty)(key)) { return fail(i, `credential key '${key}' is not allowed`); } credentialEntries.push([key, { id: cred.id ?? null, name: cred.name }]); } node.credentials = Object.fromEntries(credentialEntries); } if (op.node.disabled !== undefined) node.disabled = op.node.disabled; if (op.node.notes !== undefined) node.notes = op.node.notes; workflow.nodes.push(node); nodeByName.set(node.name, node); addedNodeNames.add(node.name); break; } case 'removeNode': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); workflow.nodes.splice(workflow.nodes.indexOf(node), 1); nodeByName.delete(op.nodeName); removeConnectionsFor(workflow.connections, op.nodeName); addedNodeNames.delete(op.nodeName); break; } case 'renameNode': { if (op.oldName === op.newName) break; if (!(0, n8n_workflow_1.isSafeObjectProperty)(op.newName)) { return fail(i, `node name '${op.newName}' is not allowed`); } const node = nodeByName.get(op.oldName); if (!node) return fail(i, `node '${op.oldName}' not found`); if (nodeByName.has(op.newName)) { return fail(i, `a node named '${op.newName}' already exists`); } node.name = op.newName; nodeByName.delete(op.oldName); nodeByName.set(op.newName, node); renameInConnections(workflow.connections, op.oldName, op.newName); if (addedNodeNames.delete(op.oldName)) addedNodeNames.add(op.newName); break; } case 'addConnection': { if (!nodeByName.has(op.source)) { return fail(i, `source node '${op.source}' not found`); } if (!nodeByName.has(op.target)) { return fail(i, `target node '${op.target}' not found`); } const connectionType = (op.connectionType ?? n8n_workflow_1.NodeConnectionTypes.Main); if (!(0, n8n_workflow_1.isSafeObjectProperty)(op.source) || !(0, n8n_workflow_1.isSafeObjectProperty)(connectionType)) { return fail(i, 'connection name is not allowed'); } const sourceIndex = op.sourceIndex ?? 0; const targetIndex = op.targetIndex ?? 0; const slot = ensureOutputSlot(workflow.connections, op.source, connectionType, sourceIndex); const exists = slot.some((c) => c.node === op.target && c.type === connectionType && c.index === targetIndex); if (!exists) { slot.push({ node: op.target, type: connectionType, index: targetIndex }); } break; } case 'removeConnection': { const connectionType = (op.connectionType ?? n8n_workflow_1.NodeConnectionTypes.Main); const sourceIndex = op.sourceIndex ?? 0; const targetIndex = op.targetIndex ?? 0; const byType = workflow.connections[op.source]; const outputs = byType?.[connectionType]; const slot = outputs?.[sourceIndex]; if (!slot) { return fail(i, `no '${connectionType}' connection from '${op.source}'`); } const filtered = slot.filter((c) => !(c.node === op.target && c.type === connectionType && c.index === targetIndex)); if (filtered.length === slot.length) { return fail(i, `connection from '${op.source}'[${sourceIndex}] to '${op.target}'[${targetIndex}] does not exist`); } outputs[sourceIndex] = filtered; pruneConnectionShape(workflow.connections, op.source, connectionType); break; } case 'setNodeCredential': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); if (!(0, n8n_workflow_1.isSafeObjectProperty)(op.credentialKey)) { return fail(i, `credential key '${op.credentialKey}' is not allowed`); } node.credentials = { ...(node.credentials ?? {}), [op.credentialKey]: { id: op.credentialId, name: op.credentialName }, }; break; } case 'setNodePosition': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); node.position = op.position; break; } case 'setNodeDisabled': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); node.disabled = op.disabled; break; } case 'setNodeSettings': { const node = nodeByName.get(op.nodeName); if (!node) return fail(i, `node '${op.nodeName}' not found`); const s = op.settings; if (s.onError !== undefined) node.onError = s.onError; if (s.retryOnFail !== undefined) node.retryOnFail = s.retryOnFail; if (s.maxTries !== undefined) node.maxTries = s.maxTries; if (s.waitBetweenTries !== undefined) node.waitBetweenTries = s.waitBetweenTries; if (s.alwaysOutputData !== undefined) node.alwaysOutputData = s.alwaysOutputData; if (s.executeOnce !== undefined) node.executeOnce = s.executeOnce; break; } case 'setWorkflowMetadata': { if (op.name !== undefined) workflow.name = op.name; if (op.description !== undefined) workflow.description = op.description; break; } default: { op; return fail(i, 'unknown operation type'); } } } return { success: true, workflow, addedNodeNames: [...addedNodeNames] }; } function toWorkflowSlice(workflow) { return { name: workflow.name ?? '', description: workflow.description, nodes: workflow.nodes, connections: workflow.connections, }; } //# sourceMappingURL=workflow-operations.js.map