diff --git a/scripts/audit-schema-coverage.ts b/scripts/audit-schema-coverage.ts new file mode 100644 index 0000000..f87ed02 --- /dev/null +++ b/scripts/audit-schema-coverage.ts @@ -0,0 +1,78 @@ +/** + * Database Schema Coverage Audit Script + * + * Audits the database to determine how many nodes have complete schema information + * for resourceLocator mode validation. This helps assess the coverage of our + * schema-driven validation approach. + */ + +import Database from 'better-sqlite3'; +import path from 'path'; + +const dbPath = path.join(__dirname, '../data/nodes.db'); +const db = new Database(dbPath, { readonly: true }); + +console.log('=== Schema Coverage Audit ===\n'); + +// Query 1: How many nodes have resourceLocator properties? +const totalResourceLocator = db.prepare(` + SELECT COUNT(*) as count FROM nodes + WHERE properties_schema LIKE '%resourceLocator%' +`).get() as { count: number }; + +console.log(`Nodes with resourceLocator properties: ${totalResourceLocator.count}`); + +// Query 2: Of those, how many have modes defined? +const withModes = db.prepare(` + SELECT COUNT(*) as count FROM nodes + WHERE properties_schema LIKE '%resourceLocator%' + AND properties_schema LIKE '%modes%' +`).get() as { count: number }; + +console.log(`Nodes with modes defined: ${withModes.count}`); + +// Query 3: Which nodes have resourceLocator but NO modes? +const withoutModes = db.prepare(` + SELECT node_type, display_name + FROM nodes + WHERE properties_schema LIKE '%resourceLocator%' + AND properties_schema NOT LIKE '%modes%' + LIMIT 10 +`).all() as Array<{ node_type: string; display_name: string }>; + +console.log(`\nSample nodes WITHOUT modes (showing 10):`); +withoutModes.forEach(node => { + console.log(` - ${node.display_name} (${node.node_type})`); +}); + +// Calculate coverage percentage +const coverage = totalResourceLocator.count > 0 + ? (withModes.count / totalResourceLocator.count) * 100 + : 0; + +console.log(`\nSchema coverage: ${coverage.toFixed(1)}% of resourceLocator nodes have modes defined`); + +// Query 4: Get some examples of nodes WITH modes for verification +console.log('\nSample nodes WITH modes (showing 5):'); +const withModesExamples = db.prepare(` + SELECT node_type, display_name + FROM nodes + WHERE properties_schema LIKE '%resourceLocator%' + AND properties_schema LIKE '%modes%' + LIMIT 5 +`).all() as Array<{ node_type: string; display_name: string }>; + +withModesExamples.forEach(node => { + console.log(` - ${node.display_name} (${node.node_type})`); +}); + +// Summary +console.log('\n=== Summary ==='); +console.log(`Total nodes in database: ${db.prepare('SELECT COUNT(*) as count FROM nodes').get() as any as { count: number }.count}`); +console.log(`Nodes with resourceLocator: ${totalResourceLocator.count}`); +console.log(`Nodes with complete mode schemas: ${withModes.count}`); +console.log(`Nodes without mode schemas: ${totalResourceLocator.count - withModes.count}`); +console.log(`\nImplication: Schema-driven validation will apply to ${withModes.count} nodes.`); +console.log(`For the remaining ${totalResourceLocator.count - withModes.count} nodes, validation will be skipped (graceful degradation).`); + +db.close(); diff --git a/src/services/config-validator.ts b/src/services/config-validator.ts index 65b0daa..1d2f768 100644 --- a/src/services/config-validator.ts +++ b/src/services/config-validator.ts @@ -276,17 +276,26 @@ export class ConfigValidator { // 1. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} } // 2. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}] const modes = prop.typeOptions.resourceLocator.modes; - let allowedModes: string[] = []; - if (typeof modes === 'object' && !Array.isArray(modes)) { - // Extract keys from modes object - allowedModes = Object.keys(modes); - } else if (Array.isArray(modes)) { - // Extract name property from array of mode objects - allowedModes = modes.map(m => typeof m === 'string' ? m : m.name).filter(Boolean); + // Validate modes structure before processing to prevent crashes + if (!modes || typeof modes !== 'object') { + // Invalid schema structure - skip validation to prevent false positives + continue; } - // Only validate if we found allowed modes + let allowedModes: string[] = []; + + if (Array.isArray(modes)) { + // Array format: extract name property from each mode object + allowedModes = modes + .map(m => (typeof m === 'object' && m !== null) ? m.name : m) + .filter(m => typeof m === 'string' && m.length > 0); + } else { + // Object format: extract keys as mode names + allowedModes = Object.keys(modes).filter(k => k.length > 0); + } + + // Only validate if we successfully extracted modes if (allowedModes.length > 0 && !allowedModes.includes(value.mode)) { errors.push({ type: 'invalid_value', diff --git a/tests/unit/services/config-validator-basic.test.ts b/tests/unit/services/config-validator-basic.test.ts index 812ade6..faa3673 100644 --- a/tests/unit/services/config-validator-basic.test.ts +++ b/tests/unit/services/config-validator-basic.test.ts @@ -713,6 +713,117 @@ describe('ConfigValidator - Basic Validation', () => { )).toBe(true); }); + it('should handle modes defined as array format', () => { + const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi'; + const config = { + model: { + mode: 'custom', + value: 'gpt-4o-mini' + } + }; + const properties = [ + { + name: 'model', + type: 'resourceLocator', + required: true, + typeOptions: { + resourceLocator: { + modes: [ + { name: 'list', displayName: 'List' }, + { name: 'id', displayName: 'ID' }, + { name: 'custom', displayName: 'Custom' } + ] + } + } + } + ]; + + const result = ConfigValidator.validate(nodeType, config, properties); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should handle malformed modes schema gracefully', () => { + const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi'; + const config = { + model: { + mode: 'any-mode', + value: 'gpt-4o-mini' + } + }; + const properties = [ + { + name: 'model', + type: 'resourceLocator', + required: true, + typeOptions: { + resourceLocator: { + modes: 'invalid-string' // Malformed schema + } + } + } + ]; + + const result = ConfigValidator.validate(nodeType, config, properties); + + // Should NOT crash, should skip validation + expect(result.valid).toBe(true); + expect(result.errors.some(e => e.property === 'model.mode')).toBe(false); + }); + + it('should handle empty modes definition gracefully', () => { + const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi'; + const config = { + model: { + mode: 'any-mode', + value: 'gpt-4o-mini' + } + }; + const properties = [ + { + name: 'model', + type: 'resourceLocator', + required: true, + typeOptions: { + resourceLocator: { + modes: {} // Empty object + } + } + } + ]; + + const result = ConfigValidator.validate(nodeType, config, properties); + + // Should skip validation with empty modes + expect(result.valid).toBe(true); + expect(result.errors.some(e => e.property === 'model.mode')).toBe(false); + }); + + it('should skip mode validation when typeOptions not provided', () => { + const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi'; + const config = { + model: { + mode: 'custom-mode', + value: 'gpt-4o-mini' + } + }; + const properties = [ + { + name: 'model', + type: 'resourceLocator', + required: true + // No typeOptions - schema doesn't define modes + } + ]; + + const result = ConfigValidator.validate(nodeType, config, properties); + + // Should accept any mode when schema doesn't define them + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + it('should accept resourceLocator with mode "url"', () => { const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi'; const config = {