mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
fix: Improve schema preview handling for trigger nodes (#23126)
This commit is contained in:
parent
efb0226ddc
commit
6ac5ee72b5
6 changed files with 276 additions and 13 deletions
|
|
@ -75,3 +75,4 @@ ignore:
|
|||
- (?s:.*/[^\/]*\.test\.ts.*)\Z
|
||||
- (?s:.*/[^\/]*e2e[^\/]*\.ts.*)\Z
|
||||
- (?s:.*/test_[^\/]*\.py.*)\Z
|
||||
- (?s:.*/scripts/.*)\Z
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
191
packages/nodes-base/scripts/validate-schema-versions.js
Normal file
191
packages/nodes-base/scripts/validate-schema-versions.js
Normal file
|
|
@ -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...');
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue