From 6ac5ee72b580fb6b06b31ab03585d08f154face2 Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Fri, 12 Dec 2025 15:26:42 +0000 Subject: [PATCH] fix: Improve schema preview handling for trigger nodes (#23126) --- codecov.yml | 1 + .../runData/components/VirtualSchema.test.ts | 69 +++++-- .../ndv/runData/components/VirtualSchema.vue | 6 + packages/nodes-base/package.json | 2 +- .../scripts/validate-schema-versions.js | 191 ++++++++++++++++++ .../editor/ndv/schema-preview.spec.ts | 20 ++ 6 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 packages/nodes-base/scripts/validate-schema-versions.js create mode 100644 packages/testing/playwright/tests/e2e/workflows/editor/ndv/schema-preview.spec.ts diff --git a/codecov.yml b/codecov.yml index 5114e8e9437..fbc8437b534 100644 --- a/codecov.yml +++ b/codecov.yml @@ -75,3 +75,4 @@ ignore: - (?s:.*/[^\/]*\.test\.ts.*)\Z - (?s:.*/[^\/]*e2e[^\/]*\.ts.*)\Z - (?s:.*/test_[^\/]*\.py.*)\Z + - (?s:.*/scripts/.*)\Z diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.test.ts b/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.test.ts index dc1c8844a9a..6d64e465389 100644 --- a/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.test.ts +++ b/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.test.ts @@ -1057,9 +1057,8 @@ describe('VirtualSchema.vue', () => { describe('execute event emission', () => { it('should emit execute event when schema preview execute link is clicked', async () => { - // Set up schema preview mode useWorkflowsStore().pinData({ - node: mockNode1, + node: mockNode2, data: [], }); @@ -1078,23 +1077,69 @@ describe('VirtualSchema.vue', () => { ); const { emitted, getByText } = renderComponent({ + props: { + nodes: [{ name: mockNode2.name, indicies: [], depth: 1 }], + }, + }); + + const executeLink = await waitFor(() => getByText('Execute the node')); + expect(executeLink).toBeInTheDocument(); + + fireEvent.click(executeLink); + + await waitFor(() => { + expect(emitted()).toHaveProperty('execute'); + expect(emitted().execute[0]).toEqual([mockNode2.name]); + }); + }); + }); + + describe('trigger node schema preview', () => { + it('should not call getSchemaPreview for trigger nodes', async () => { + const schemaPreviewStore = useSchemaPreviewStore(); + const getSchemaPreviewSpy = vi.spyOn(schemaPreviewStore, 'getSchemaPreview'); + + const { getAllByText } = renderComponent({ props: { nodes: [{ name: mockNode1.name, indicies: [], depth: 1 }], }, }); - // Wait for the preview execute link to appear - const executeLink = await waitFor(() => getByText('Execute the node')); - expect(executeLink).toBeInTheDocument(); - - // Click the execute link - fireEvent.click(executeLink); - - // Verify the execute event was emitted with the node name await waitFor(() => { - expect(emitted()).toHaveProperty('execute'); - expect(emitted().execute[0]).toEqual([mockNode1.name]); + expect(getAllByText('Execute previous nodes').length).toBe(1); }); + + expect(getSchemaPreviewSpy).not.toHaveBeenCalled(); + }); + + it('should call getSchemaPreview for non-trigger nodes without data', async () => { + const schemaPreviewStore = useSchemaPreviewStore(); + const getSchemaPreviewSpy = vi + .spyOn(schemaPreviewStore, 'getSchemaPreview') + .mockResolvedValue( + createResultOk({ + type: 'object', + properties: { + id: { type: 'string' }, + }, + }), + ); + + const { getAllByTestId } = renderComponent({ + props: { + nodes: [{ name: mockNode2.name, indicies: [], depth: 1 }], + }, + }); + + await waitFor(() => { + expect(getAllByTestId('run-data-schema-header')).toHaveLength(2); + }); + + expect(getSchemaPreviewSpy).toHaveBeenCalledWith( + expect.objectContaining({ + nodeType: SET_NODE_TYPE, + }), + ); }); }); diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.vue b/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.vue index e03b1b8f3f9..2ba9a15cee3 100644 --- a/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.vue +++ b/packages/frontend/editor-ui/src/features/ndv/runData/components/VirtualSchema.vue @@ -286,6 +286,12 @@ const nodeSchema = asyncComputed(async () => { async function getSchemaPreview(node: INodeUi | null) { if (!node) return createResultError(new Error()); + + const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); + if (nodeType?.group.includes('trigger')) { + return createResultError(new Error('Trigger nodes do not have schema previews')); + } + const { type, typeVersion, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 745a8d5e51d..dad64e7ba63 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -11,7 +11,7 @@ "build": "tsc --build tsconfig.build.cjs.json && pnpm copy-nodes-json && tsc-alias -p tsconfig.build.cjs.json && pnpm n8n-copy-static-files && pnpm n8n-generate-translations && pnpm n8n-generate-metadata", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint nodes credentials utils test --quiet && node ./scripts/validate-load-options-methods.js", + "lint": "eslint nodes credentials utils test --quiet && node ./scripts/validate-load-options-methods.js && node ./scripts/validate-schema-versions.js", "lint:fix": "eslint nodes credentials utils test --fix", "watch": "tsc-watch -p tsconfig.build.cjs.json --onCompilationComplete \"pnpm copy-nodes-json && tsc-alias -p tsconfig.build.cjs.json\" --onSuccess \"pnpm n8n-generate-metadata\"", "test": "jest", diff --git a/packages/nodes-base/scripts/validate-schema-versions.js b/packages/nodes-base/scripts/validate-schema-versions.js new file mode 100644 index 00000000000..a93498211ba --- /dev/null +++ b/packages/nodes-base/scripts/validate-schema-versions.js @@ -0,0 +1,191 @@ +/** + * Validates that schema files exist for all declared node versions. + * + * This script checks nodes that have __schema__ directories and verifies + * that schema files exist for the node's default version. This prevents + * issues where a node version is bumped but schema files are not updated. + * + * Run as part of: pnpm lint + */ + +const fs = require('fs'); +const path = require('path'); + +const NODES_DIR = path.join(__dirname, '../nodes'); +const DIST_DIR = path.join(__dirname, '../dist/nodes'); + +/** + * Recursively find all directories containing __schema__ + */ +function findSchemaDirectories(dir, results = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === '__schema__') { + results.push(path.dirname(fullPath)); + } else { + findSchemaDirectories(fullPath, results); + } + } + } + + return results; +} + +/** + * Get available schema versions from __schema__ directory + */ +function getAvailableSchemaVersions(nodeDir) { + const schemaDir = path.join(nodeDir, '__schema__'); + if (!fs.existsSync(schemaDir)) return []; + + return fs + .readdirSync(schemaDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && entry.name.startsWith('v')) + .map((entry) => entry.name.replace('v', '')); +} + +/** + * Find the main node file in a directory + */ +function findNodeFile(nodeDir) { + const entries = fs.readdirSync(nodeDir); + const nodeFile = entries.find( + (f) => f.endsWith('.node.ts') && !f.includes('Trigger') && !f.includes('.test.'), + ); + return nodeFile ? path.join(nodeDir, nodeFile) : null; +} + +/** + * Extract version info from node source file using regex + * This handles both simple version: X and defaultVersion: X patterns + */ +function extractVersionFromSource(nodeFilePath) { + const content = fs.readFileSync(nodeFilePath, 'utf8'); + + // Check for defaultVersion (versioned nodes) + const defaultVersionMatch = content.match(/defaultVersion:\s*([\d.]+)/); + if (defaultVersionMatch) { + return { + defaultVersion: parseFloat(defaultVersionMatch[1]), + isVersioned: true, + }; + } + + // Check for simple version (non-versioned nodes) + const versionMatch = content.match(/version:\s*(\[[\d\s,.]+\]|[\d.]+)/); + if (versionMatch) { + const versionValue = versionMatch[1]; + // Handle array format: version: [1, 2, 3] + if (versionValue.startsWith('[')) { + const versions = versionValue + .replace(/[\[\]]/g, '') + .split(',') + .map((v) => parseFloat(v.trim())) + .filter((v) => !isNaN(v)); + return { + defaultVersion: Math.max(...versions), + isVersioned: false, + }; + } + return { + defaultVersion: parseFloat(versionValue), + isVersioned: false, + }; + } + + return null; +} + +/** + * Convert version number to schema directory format (e.g., 2.2 -> "2.2.0") + */ +function versionToSchemaFormat(version) { + const parts = version.toString().split('.'); + while (parts.length < 3) parts.push('0'); + return parts.slice(0, 3).join('.'); +} + +/** + * Main validation function + */ +function validateSchemaVersions() { + console.log('Validating schema versions for nodes...\n'); + + const nodesWithSchemas = findSchemaDirectories(NODES_DIR); + const errors = []; + const warnings = []; + + for (const nodeDir of nodesWithSchemas) { + const relativePath = path.relative(NODES_DIR, nodeDir); + const nodeFile = findNodeFile(nodeDir); + + if (!nodeFile) { + // Try parent directory for versioned nodes + const parentNodeFile = findNodeFile(path.dirname(nodeDir)); + if (!parentNodeFile) { + warnings.push(`${relativePath}: Could not find node file`); + continue; + } + } + + const actualNodeFile = nodeFile || findNodeFile(path.dirname(nodeDir)); + if (!actualNodeFile) { + warnings.push(`${relativePath}: Could not find node file`); + continue; + } + const versionInfo = extractVersionFromSource(actualNodeFile); + + if (!versionInfo) { + warnings.push(`${relativePath}: Could not extract version info`); + continue; + } + + const availableSchemas = getAvailableSchemaVersions(nodeDir); + const expectedSchemaVersion = versionToSchemaFormat(versionInfo.defaultVersion); + + if (!availableSchemas.includes(expectedSchemaVersion)) { + const nodeName = path.basename(actualNodeFile, '.node.ts'); + errors.push({ + node: nodeName, + path: relativePath, + defaultVersion: versionInfo.defaultVersion, + expectedSchema: `v${expectedSchemaVersion}`, + availableSchemas: availableSchemas.map((v) => `v${v}`), + }); + } + } + + // Report results + if (errors.length > 0) { + console.warn('⚠️ WARNING: The following nodes have missing schema versions:\n'); + for (const error of errors) { + console.warn(` ${error.node} (${error.path})`); + console.warn(` Default version: ${error.defaultVersion}`); + console.warn(` Expected schema: ${error.expectedSchema}`); + console.warn(` Available schemas: ${error.availableSchemas.join(', ') || 'none'}`); + console.warn(''); + } + console.warn('Schema files should be generated for these versions.'); + console.warn(''); + } + + if (warnings.length > 0) { + console.warn('⚠️ Warnings:\n'); + for (const warning of warnings) { + console.warn(` ${warning}`); + } + console.warn(''); + } + + console.log(`✅ All ${nodesWithSchemas.length} nodes with schemas have valid version mappings.`); +} + +try { + validateSchemaVersions(); +} catch (error) { + console.warn('⚠️ Schema validation script encountered an error:', error.message); + console.warn('Continuing without blocking...'); +} diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/schema-preview.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/schema-preview.spec.ts new file mode 100644 index 00000000000..7117d34b13b --- /dev/null +++ b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/schema-preview.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '../../../../../fixtures/base'; + +test.describe('Schema Preview', () => { + test('should show schema preview for regular nodes but not triggers', async ({ n8n }) => { + await n8n.start.fromBlankCanvas(); + + await n8n.canvas.addNode('Gmail', { trigger: 'On message received' }); + await n8n.ndv.close(); + + await n8n.canvas.addNode('Edit Fields (Set)'); + await n8n.ndv.inputPanel.get().getByText('No input data').waitFor(); + await n8n.ndv.close(); + + await n8n.canvas.addNode('Hacker News', { action: 'Get an article' }); + await n8n.ndv.close(); + + await n8n.canvas.addNode('Edit Fields (Set)'); + await expect(n8n.ndv.inputPanel.getSchemaItem('author')).toBeVisible(); + }); +});