fix: enable schema-based resourceLocator mode validation

Root cause analysis revealed validator was looking at wrong path for
modes data. n8n stores modes at top level of properties, not nested
in typeOptions.

Changes:
- config-validator.ts: Changed from prop.typeOptions?.resourceLocator?.modes
  to prop.modes (lines 273-310)
- property-extractor.ts: Added modes field to normalizeProperties to
  capture mode definitions from n8n nodes
- Updated all test cases to match real n8n schema structure with modes
  at property top level
- Rebuilt database with modes field

Results:
- 100% coverage: All 70 resourceLocator nodes now have modes defined
- Schema-based validation now ACTIVE (was being skipped before)
- False positive eliminated: Google Sheets "name" mode now validates
- Helpful error messages showing actual allowed modes from schema

Testing:
- All 33 unit tests pass
- Verified with n8n-mcp-tester: valid "name" mode passes, invalid modes
  fail with clear error listing allowed options [list, url, id, name]

Fixes #304 (Google Sheets false positive)
Related to #306 (validator improvements)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-10-11 19:29:21 +02:00
parent 4625ebf64d
commit fc8fb66900
4 changed files with 24 additions and 36 deletions

Binary file not shown.

View File

@@ -231,6 +231,7 @@ export class PropertyExtractor {
required: prop.required, required: prop.required,
displayOptions: prop.displayOptions, displayOptions: prop.displayOptions,
typeOptions: prop.typeOptions, typeOptions: prop.typeOptions,
modes: prop.modes, // For resourceLocator type properties - modes are at top level
noDataExpression: prop.noDataExpression noDataExpression: prop.noDataExpression
})); }));
} }

View File

@@ -270,12 +270,13 @@ export class ConfigValidator {
message: `resourceLocator '${key}.mode' must be a string, got ${typeof value.mode}`, message: `resourceLocator '${key}.mode' must be a string, got ${typeof value.mode}`,
fix: `Set mode to a valid string value` fix: `Set mode to a valid string value`
}); });
} else if (prop.typeOptions?.resourceLocator?.modes) { } else if (prop.modes) {
// Schema-based validation: Check if mode exists in the modes definition // Schema-based validation: Check if mode exists in the modes definition
// In n8n, modes are defined at the top level of resourceLocator properties
// Modes can be defined in different ways: // Modes can be defined in different ways:
// 1. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} } // 1. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}, {name: 'name', ...}]
// 2. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}] // 2. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} }
const modes = prop.typeOptions.resourceLocator.modes; const modes = prop.modes;
// Validate modes structure before processing to prevent crashes // Validate modes structure before processing to prevent crashes
if (!modes || typeof modes !== 'object') { if (!modes || typeof modes !== 'object') {
@@ -286,7 +287,7 @@ export class ConfigValidator {
let allowedModes: string[] = []; let allowedModes: string[] = [];
if (Array.isArray(modes)) { if (Array.isArray(modes)) {
// Array format: extract name property from each mode object // Array format (most common in n8n): extract name property from each mode object
allowedModes = modes allowedModes = modes
.map(m => (typeof m === 'object' && m !== null) ? m.name : m) .map(m => (typeof m === 'object' && m !== null) ? m.name : m)
.filter(m => typeof m === 'string' && m.length > 0); .filter(m => typeof m === 'string' && m.length > 0);
@@ -305,7 +306,7 @@ export class ConfigValidator {
}); });
} }
} }
// If no typeOptions.resourceLocator.modes defined, skip mode validation // If no modes defined at property level, skip mode validation
// This prevents false positives for nodes with dynamic/runtime-determined modes // This prevents false positives for nodes with dynamic/runtime-determined modes
if (value.value === undefined) { if (value.value === undefined) {

View File

@@ -691,15 +691,12 @@ describe('ConfigValidator - Basic Validation', () => {
name: 'model', name: 'model',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
typeOptions: { // In real n8n, modes are at top level, not in typeOptions
resourceLocator: { modes: [
modes: { { name: 'list', displayName: 'List' },
list: { displayName: 'List' }, { name: 'id', displayName: 'ID' },
id: { displayName: 'ID' }, { name: 'url', displayName: 'URL' }
url: { displayName: 'URL' } ]
}
}
}
} }
]; ];
@@ -726,15 +723,12 @@ describe('ConfigValidator - Basic Validation', () => {
name: 'model', name: 'model',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
typeOptions: { // Array format at top level (real n8n structure)
resourceLocator: { modes: [
modes: [ { name: 'list', displayName: 'List' },
{ name: 'list', displayName: 'List' }, { name: 'id', displayName: 'ID' },
{ name: 'id', displayName: 'ID' }, { name: 'custom', displayName: 'Custom' }
{ name: 'custom', displayName: 'Custom' } ]
]
}
}
} }
]; ];
@@ -757,11 +751,7 @@ describe('ConfigValidator - Basic Validation', () => {
name: 'model', name: 'model',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
typeOptions: { modes: 'invalid-string' // Malformed schema at top level
resourceLocator: {
modes: 'invalid-string' // Malformed schema
}
}
} }
]; ];
@@ -785,11 +775,7 @@ describe('ConfigValidator - Basic Validation', () => {
name: 'model', name: 'model',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
typeOptions: { modes: {} // Empty object at top level
resourceLocator: {
modes: {} // Empty object
}
}
} }
]; ];
@@ -800,7 +786,7 @@ describe('ConfigValidator - Basic Validation', () => {
expect(result.errors.some(e => e.property === 'model.mode')).toBe(false); expect(result.errors.some(e => e.property === 'model.mode')).toBe(false);
}); });
it('should skip mode validation when typeOptions not provided', () => { it('should skip mode validation when modes not provided', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi'; const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = { const config = {
model: { model: {
@@ -813,7 +799,7 @@ describe('ConfigValidator - Basic Validation', () => {
name: 'model', name: 'model',
type: 'resourceLocator', type: 'resourceLocator',
required: true required: true
// No typeOptions - schema doesn't define modes // No modes property - schema doesn't define modes
} }
]; ];