n8n
Version:
n8n Workflow Automation Tool
301 lines (300 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generatePinData = generatePinData;
const fs_1 = require("fs");
const path_1 = require("path");
const instance_ai_1 = require("@n8n/instance-ai");
let _resolvedNodesBasePath;
function resolveNodesBasePath() {
if (_resolvedNodesBasePath !== undefined) {
return _resolvedNodesBasePath ?? undefined;
}
let dir = __dirname;
for (let i = 0; i < 10; i++) {
const candidate = (0, path_1.join)(dir, 'packages', 'nodes-base', 'nodes');
if ((0, fs_1.existsSync)(candidate)) {
_resolvedNodesBasePath = candidate;
return candidate;
}
const parent = (0, path_1.join)(dir, '..');
if (parent === dir)
break;
dir = parent;
}
_resolvedNodesBasePath = null;
return undefined;
}
const schemaMapCache = new Map();
function buildSchemaMap(nodesBasePath) {
const cached = schemaMapCache.get(nodesBasePath);
if (cached)
return cached;
const result = new Map();
function scanDir(dir) {
try {
for (const entry of (0, fs_1.readdirSync)(dir, { withFileTypes: true })) {
if (!entry.isDirectory())
continue;
const entryPath = (0, path_1.join)(dir, entry.name);
const schemaDir = (0, path_1.join)(entryPath, '__schema__');
if ((0, fs_1.existsSync)(schemaDir)) {
const nodeFiles = (0, fs_1.readdirSync)(entryPath).filter((f) => f.endsWith('.node.ts') || f.endsWith('.node.js'));
for (const nodeFile of nodeFiles) {
try {
const content = (0, fs_1.readFileSync)((0, path_1.join)(entryPath, nodeFile), 'utf-8');
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
if (nameMatch) {
result.set(`n8n-nodes-base.${nameMatch[1]}`, entryPath);
}
}
catch {
}
}
}
scanDir(entryPath);
}
}
catch {
}
}
scanDir(nodesBasePath);
schemaMapCache.set(nodesBasePath, result);
return result;
}
function normalizeVersion(version) {
const str = String(version);
const parts = str.split('.');
while (parts.length < 3)
parts.push('0');
return parts.join('.');
}
function resolveSchemaForNode(nodeType, typeVersion, resource, operation, nodesBasePath) {
const schemaMap = buildSchemaMap(nodesBasePath);
const nodeDir = schemaMap.get(nodeType);
if (!nodeDir)
return undefined;
const schemaBaseDir = (0, path_1.join)(nodeDir, '__schema__');
if (!(0, fs_1.existsSync)(schemaBaseDir))
return undefined;
const versionStr = normalizeVersion(typeVersion);
const versionDirs = [
`v${versionStr}`,
...(0, fs_1.readdirSync)(schemaBaseDir)
.filter((d) => d.startsWith('v'))
.sort((a, b) => {
const partsA = a.slice(1).split('.').map(Number);
const partsB = b.slice(1).split('.').map(Number);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const diff = (partsB[i] ?? 0) - (partsA[i] ?? 0);
if (diff !== 0)
return diff;
}
return 0;
}),
];
for (const vDir of [...new Set(versionDirs)]) {
const versionPath = (0, path_1.join)(schemaBaseDir, vDir);
if (!(0, fs_1.existsSync)(versionPath))
continue;
const parts = [versionPath, resource, operation ? `${operation}.json` : undefined].filter(Boolean);
const schemaFile = (0, path_1.join)(...parts);
if ((0, fs_1.existsSync)(schemaFile)) {
try {
return JSON.parse((0, fs_1.readFileSync)(schemaFile, 'utf-8'));
}
catch {
return undefined;
}
}
}
return undefined;
}
function buildSchemaContexts(nodes, nodesBasePath) {
return nodes.map((node) => {
const params = node.parameters;
const resource = typeof params?.resource === 'string' ? params.resource : undefined;
const operation = typeof params?.operation === 'string' ? params.operation : undefined;
let schema;
if (nodesBasePath) {
schema = resolveSchemaForNode(node.type, node.typeVersion, resource, operation, nodesBasePath);
}
return {
nodeName: node.name ?? node.type,
nodeType: node.type,
typeVersion: node.typeVersion,
resource,
operation,
schema,
};
});
}
function workflowToMermaid(workflow) {
const lines = ['flowchart LR'];
const nodeIdMap = new Map();
workflow.nodes.forEach((node, i) => {
if (node.name)
nodeIdMap.set(node.name, `n${String(i)}`);
});
for (const node of workflow.nodes) {
if (!node.name)
continue;
const id = nodeIdMap.get(node.name);
if (!id)
continue;
const params = node.parameters;
const resource = typeof params?.resource === 'string' ? params.resource : undefined;
const operation = typeof params?.operation === 'string' ? params.operation : undefined;
const shortType = node.type.split('.').pop() ?? node.type;
let label = `${node.name} (${shortType} v${String(node.typeVersion)}`;
if (resource)
label += `, resource:${resource}`;
if (operation)
label += `, op:${operation}`;
label += ')';
lines.push(` ${id}["${label}"]`);
}
const { connections } = workflow;
for (const [sourceName, nodeConns] of Object.entries(connections)) {
const sourceId = nodeIdMap.get(sourceName);
if (!sourceId)
continue;
for (const [, outputConnections] of Object.entries(nodeConns)) {
if (!Array.isArray(outputConnections))
continue;
for (const outputGroup of outputConnections) {
if (!Array.isArray(outputGroup))
continue;
for (const conn of outputGroup) {
if (typeof conn !== 'object' || conn === null || !('node' in conn))
continue;
const targetId = nodeIdMap.get(conn.node);
if (targetId) {
lines.push(` ${sourceId} --> ${targetId}`);
}
}
}
}
}
return lines.join('\n');
}
const SYSTEM_PROMPT = `You are a test data generator for n8n workflow automation. Generate realistic mock API response data for service nodes in a workflow.
RULES:
1. Data must be consistent across nodes. If node A creates an entity with id "abc-123", downstream nodes referencing that entity must use "abc-123".
2. Generate 1-2 items per node.
3. When a JSON Schema is provided, follow its structure exactly.
4. When no schema is provided, generate a realistic response based on the node type, resource, and operation.
5. Use realistic but clearly fake values (e.g., "jane@example.com", "proj_abc123", "2024-01-15T10:30:00Z").
6. Return ONLY a valid JSON object, no explanation or markdown fencing.
7. CRITICAL: You MUST generate data for EVERY node listed in "Nodes Requiring Mock Data". Never skip a node, even if the test scenario describes an empty or error response. An empty response is still valid data.`;
function buildUserPrompt(workflow, contexts, instructions) {
const mermaid = workflowToMermaid(workflow);
const sections = ['Generate mock output data for service nodes in this workflow.'];
if (instructions?.dataDescription) {
sections.push('');
sections.push('## Data Generation Instructions');
sections.push('');
sections.push(instructions.dataDescription);
}
sections.push('');
sections.push('## Workflow Graph');
sections.push('');
sections.push('```mermaid');
sections.push(mermaid);
sections.push('```');
sections.push('');
sections.push('## Nodes Requiring Mock Data');
for (const ctx of contexts) {
sections.push('');
sections.push(`### ${ctx.nodeName} (${ctx.nodeType} v${String(ctx.typeVersion)})`);
if (ctx.resource || ctx.operation) {
const parts = [];
if (ctx.resource)
parts.push(`Resource: ${ctx.resource}`);
if (ctx.operation)
parts.push(`Operation: ${ctx.operation}`);
sections.push(`- ${parts.join(' | ')}`);
}
if (ctx.schema) {
const schemaStr = JSON.stringify(ctx.schema, null, 2);
const truncated = schemaStr.length > 3000 ? schemaStr.slice(0, 3000) + '\n...' : schemaStr;
sections.push('- Output JSON Schema:');
sections.push('```json');
sections.push(truncated);
sections.push('```');
}
else {
sections.push('(no schema available — generate based on API knowledge)');
}
}
sections.push('');
sections.push('## Expected Output Format');
sections.push('');
sections.push('Return a JSON object where each key is the exact node name and the value is an array of items, each wrapped in a "json" key:');
sections.push('');
sections.push('```json');
sections.push('{');
for (let i = 0; i < Math.min(contexts.length, 2); i++) {
const ctx = contexts[i];
const comma = i < Math.min(contexts.length, 2) - 1 ? ',' : '';
sections.push(` "${ctx.nodeName}": [{ "json": { ... } }]${comma}`);
}
if (contexts.length > 2) {
sections.push(' ...');
}
sections.push('}');
sections.push('```');
return sections.join('\n');
}
function parsePinDataResponse(responseText, expectedNodes) {
let cleaned = responseText.trim();
if (cleaned.startsWith('```')) {
cleaned = cleaned.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
}
let parsed;
try {
parsed = JSON.parse(cleaned);
}
catch {
return {};
}
const pinData = {};
for (const nodeName of expectedNodes) {
const nodeData = parsed[nodeName];
if (!Array.isArray(nodeData) || nodeData.length === 0)
continue;
pinData[nodeName] = nodeData.map((item) => {
if (typeof item === 'object' && item !== null && 'json' in item) {
return item;
}
return { json: item ?? {} };
});
}
return pinData;
}
async function generatePinData(options) {
const { workflow, nodeNames, instructions } = options;
if (nodeNames.length === 0)
return {};
const targetNodes = workflow.nodes.filter((n) => n.name && nodeNames.includes(n.name));
if (targetNodes.length === 0)
return {};
const nodesBasePath = resolveNodesBasePath();
const contexts = buildSchemaContexts(targetNodes, nodesBasePath);
const userPrompt = buildUserPrompt(workflow, contexts, instructions);
const expectedNodeNames = contexts.map((c) => c.nodeName);
try {
const agent = (0, instance_ai_1.createEvalAgent)('eval-pin-data-generator', {
instructions: SYSTEM_PROMPT,
cache: true,
});
const result = await agent.generate(userPrompt, {
providerOptions: { anthropic: { maxTokens: 16_384 } },
});
const responseText = (0, instance_ai_1.extractText)(result);
return parsePinDataResponse(responseText, expectedNodeNames);
}
catch {
return {};
}
}
//# sourceMappingURL=pin-data-generator.js.map