UNPKG

code-auditor-mcp

Version:

Multi-language code quality auditor with MCP server - Analyze TypeScript, JavaScript, and Go code for SOLID principles, DRY violations, security patterns, and more

434 lines 16 kB
/** * Database Schema Parser and Validator * Handles loading, parsing, and validating database schema definitions */ import { promises as fs } from 'fs'; import path from 'path'; // import yaml from 'js-yaml'; // Temporary workaround for missing dependency const yaml = { load: (content) => { throw new Error('YAML parsing not available - js-yaml dependency not installed'); } }; export class SchemaParser { static SUPPORTED_FORMATS = ['.json', '.yaml', '.yml']; static DATABASE_TYPES = [ 'postgresql', 'mysql', 'mongodb', 'sqlite', 'redis', 'dynamodb', 'other' ]; /** * Parse schema from file */ async parseFromFile(filePath, options = {}) { try { const ext = path.extname(filePath).toLowerCase(); if (!SchemaParser.SUPPORTED_FORMATS.includes(ext)) { return { success: false, violations: [], errors: [`Unsupported file format: ${ext}. Supported formats: ${SchemaParser.SUPPORTED_FORMATS.join(', ')}`] }; } const content = await fs.readFile(filePath, 'utf-8'); return this.parseFromString(content, ext, options); } catch (error) { return { success: false, violations: [], errors: [`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`] }; } } /** * Parse schema from string content */ async parseFromString(content, format, options = {}) { const violations = []; const errors = []; try { let parsed; if (format === '.json') { parsed = JSON.parse(content); } else if (format === '.yaml' || format === '.yml') { parsed = yaml.load(content); } else { return { success: false, violations, errors: [`Unsupported format: ${format}`] }; } // Validate structure const structureValidation = this.validateStructure(parsed); if (!structureValidation.valid) { return { success: false, violations, errors: structureValidation.errors }; } // Convert to typed schema const schema = this.normalizeSchema(parsed); // Validate schema content const contentViolations = this.validateSchema(schema, options); violations.push(...contentViolations); return { success: true, schema, violations, errors }; } catch (error) { return { success: false, violations, errors: [`Parse error: ${error instanceof Error ? error.message : 'Unknown error'}`] }; } } /** * Validate basic structure of parsed object */ validateStructure(obj) { const errors = []; if (!obj || typeof obj !== 'object') { errors.push('Schema must be an object'); return { valid: false, errors }; } // Required fields if (!obj.version) errors.push('Missing required field: version'); if (!obj.name) errors.push('Missing required field: name'); if (!obj.databases || !Array.isArray(obj.databases)) { errors.push('Missing or invalid field: databases (must be an array)'); } // Validate databases structure if (obj.databases && Array.isArray(obj.databases)) { obj.databases.forEach((db, index) => { if (!db.name) errors.push(`Database ${index}: missing name`); if (!db.type) errors.push(`Database ${index}: missing type`); if (!db.tables || !Array.isArray(db.tables)) { errors.push(`Database ${index}: missing or invalid tables array`); } }); } return { valid: errors.length === 0, errors }; } /** * Normalize parsed object to typed schema */ normalizeSchema(obj) { return { version: obj.version, name: obj.name, description: obj.description, databases: obj.databases.map((db) => this.normalizeDatabase(db)), globalReferences: obj.globalReferences || [], metadata: { author: obj.metadata?.author, createdAt: obj.metadata?.createdAt ? new Date(obj.metadata.createdAt) : undefined, updatedAt: obj.metadata?.updatedAt ? new Date(obj.metadata.updatedAt) : undefined, tags: obj.metadata?.tags || [], environment: obj.metadata?.environment } }; } /** * Normalize database object */ normalizeDatabase(db) { return { name: db.name, type: db.type, version: db.version, host: db.host, port: db.port, database: db.database, schemas: db.schemas || [], tables: (db.tables || []).map((table) => this.normalizeTable(table)), relationships: db.relationships || [], description: db.description, createdAt: db.createdAt ? new Date(db.createdAt) : undefined, updatedAt: db.updatedAt ? new Date(db.updatedAt) : undefined, metadata: db.metadata }; } /** * Normalize table object */ normalizeTable(table) { return { name: table.name, type: table.type || 'table', database: table.database, schema: table.schema, columns: (table.columns || []).map((col) => this.normalizeColumn(col)), references: table.references || [], indexes: table.indexes || [], constraints: table.constraints || [], description: table.description, tags: table.tags || [], estimatedRows: table.estimatedRows, isTemporary: table.isTemporary || false, partitionKey: table.partitionKey }; } /** * Normalize column object */ normalizeColumn(col) { return { name: col.name, type: col.type, nullable: col.nullable !== false, // Default to true if not specified primaryKey: col.primaryKey || false, defaultValue: col.defaultValue, unique: col.unique || false, indexed: col.indexed || false, length: col.length, precision: col.precision, scale: col.scale, description: col.description, enum: col.enum }; } /** * Validate schema content and relationships */ validateSchema(schema, options) { const violations = []; for (const database of schema.databases) { violations.push(...this.validateDatabase(database, options)); } return violations; } /** * Validate individual database */ validateDatabase(database, options) { const violations = []; // Check database type if (!SchemaParser.DATABASE_TYPES.includes(database.type)) { violations.push({ file: 'schema', severity: 'warning', message: `Unknown database type: ${database.type}`, schemaType: 'naming-convention', details: `Supported types: ${SchemaParser.DATABASE_TYPES.join(', ')}` }); } // Validate tables for (const table of database.tables) { violations.push(...this.validateTable(table, database, options)); } // Check for orphaned references if (options.checkReferences !== false) { violations.push(...this.validateReferences(database)); } return violations; } /** * Validate individual table */ validateTable(table, database, options) { const violations = []; // Check for primary key const hasPrimaryKey = table.columns.some(col => col.primaryKey); if (!hasPrimaryKey && table.type === 'table') { violations.push({ file: 'schema', severity: 'warning', message: `Table '${table.name}' has no primary key`, schemaType: 'missing-index', tableName: table.name }); } // Check naming conventions if (options.checkNamingConventions !== false) { violations.push(...this.validateTableNaming(table)); } // Validate columns for (const column of table.columns) { violations.push(...this.validateColumn(column, table)); } return violations; } /** * Validate column definitions */ validateColumn(column, table) { const violations = []; // Check for missing type if (!column.type) { violations.push({ file: 'schema', severity: 'critical', message: `Column '${column.name}' in table '${table.name}' is missing type`, schemaType: 'missing-reference', tableName: table.name, columnName: column.name }); } // Check for enum without values if (column.type === 'enum' && (!column.enum || column.enum.length === 0)) { violations.push({ file: 'schema', severity: 'warning', message: `Enum column '${column.name}' in table '${table.name}' has no values defined`, schemaType: 'missing-reference', tableName: table.name, columnName: column.name }); } return violations; } /** * Validate table naming conventions */ validateTableNaming(table) { const violations = []; // Check snake_case convention if (!/^[a-z][a-z0-9_]*$/.test(table.name)) { violations.push({ file: 'schema', severity: 'suggestion', message: `Table name '${table.name}' should follow snake_case convention`, schemaType: 'naming-convention', tableName: table.name, suggestion: 'Use lowercase letters, numbers, and underscores only' }); } // Check for plural table names if (!table.name.endsWith('s') && !table.name.includes('_')) { violations.push({ file: 'schema', severity: 'suggestion', message: `Table name '${table.name}' should typically be plural`, schemaType: 'naming-convention', tableName: table.name, suggestion: 'Consider using plural form for table names' }); } return violations; } /** * Validate references across tables */ validateReferences(database) { const violations = []; const tableNames = new Set(database.tables.map(t => t.name)); for (const table of database.tables) { for (const reference of table.references) { // Check if referenced table exists if (!tableNames.has(reference.referencedTable)) { violations.push({ file: 'schema', severity: 'critical', message: `Reference in table '${table.name}' points to non-existent table '${reference.referencedTable}'`, schemaType: 'missing-reference', tableName: table.name, details: `Foreign key: ${reference.foreignKey} -> ${reference.referencedTable}.${reference.referencedColumn}` }); continue; } // Check if referenced column exists const referencedTable = database.tables.find(t => t.name === reference.referencedTable); if (referencedTable) { const referencedColumn = referencedTable.columns.find(c => c.name === reference.referencedColumn); if (!referencedColumn) { violations.push({ file: 'schema', severity: 'critical', message: `Reference in table '${table.name}' points to non-existent column '${reference.referencedColumn}' in table '${reference.referencedTable}'`, schemaType: 'missing-reference', tableName: table.name, columnName: reference.referencedColumn, details: `Foreign key: ${reference.foreignKey} -> ${reference.referencedTable}.${reference.referencedColumn}` }); } } // Check if foreign key column exists in current table const foreignKeyColumn = table.columns.find(c => c.name === reference.foreignKey); if (!foreignKeyColumn) { violations.push({ file: 'schema', severity: 'critical', message: `Foreign key column '${reference.foreignKey}' not found in table '${table.name}'`, schemaType: 'missing-reference', tableName: table.name, columnName: reference.foreignKey }); } } } return violations; } /** * Get schema statistics */ getSchemaStats(schema) { let tableCount = 0; let columnCount = 0; let referenceCount = 0; let indexCount = 0; for (const database of schema.databases) { tableCount += database.tables.length; for (const table of database.tables) { columnCount += table.columns.length; referenceCount += table.references.length; indexCount += table.indexes?.length || 0; } } return { databaseCount: schema.databases.length, tableCount, columnCount, referenceCount, indexCount }; } /** * Find circular dependencies in schema */ findCircularDependencies(database) { const cycles = []; const visited = new Set(); const path = []; const dfs = (tableName) => { if (path.includes(tableName)) { // Found cycle const cycleStart = path.indexOf(tableName); const cycle = [...path.slice(cycleStart), tableName]; cycles.push(cycle); return true; } if (visited.has(tableName)) return false; visited.add(tableName); path.push(tableName); const table = database.tables.find(t => t.name === tableName); if (table) { for (const ref of table.references) { if (dfs(ref.referencedTable)) return true; } } path.pop(); return false; }; for (const table of database.tables) { if (!visited.has(table.name)) { dfs(table.name); } } return cycles; } } //# sourceMappingURL=SchemaParser.js.map