UNPKG

morpheus4j

Version:

Morpheus is a migration tool for Neo4j. It aims to be a simple and intuitive way to migrate your database.

215 lines 9.76 kB
"use strict"; /* eslint-disable complexity */ /* eslint-disable max-depth */ /* eslint-disable no-await-in-loop */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ValidateService = void 0; const kleur_1 = require("kleur"); const errors_1 = require("../errors"); const logger_1 = require("./logger"); const utils_1 = require("./utils"); class ValidateService { repository; fileService; static MESSAGES = { DB_CONNECT_ERROR: 'Failed to connect to the database', NO_MIGRATIONS: 'No migrations found in database or local directory', VALIDATION_ERROR: 'Migration validation failed', VALIDATION_SUCCESS: 'All migrations are valid', }; constructor(repository, fileService) { this.repository = repository; this.fileService = fileService; } async validate(options = {}) { try { const [state, files] = await Promise.all([ this.repository.getMigrationState(), this.fileService.getFileNamesFromMigrationsFolder(), ]); const { appliedMigrations } = state; if (appliedMigrations.length === 0 && files.length === 0) { logger_1.Logger.info(ValidateService.MESSAGES.NO_MIGRATIONS); return { failures: [], isValid: true }; } const failures = []; for (const migration of appliedMigrations) { const { source: fileName, version } = migration.node; if (!files.includes(fileName)) { const failure = { message: `Migration ${version} exists in database but file ${fileName} is missing locally`, severity: 'ERROR', type: 'MISSING_FILE', version, }; failures.push(failure); if (options.failFast && failures.length > 0) { break; } } } if (options.failFast && failures.length > 0) { return this.reportResult(failures, options); } const dbVersions = appliedMigrations.map((m) => m.node.version); const fileVersions = []; for (const fileName of files) { const version = this.fileService.getMigrationVersionFromFileName(fileName); fileVersions.push(version); if (version === 'BASELINE') continue; if (!dbVersions.includes(version)) { const failure = { message: `Migration file ${fileName} exists locally but has not been applied to the database`, severity: 'ERROR', type: 'MISSING_DB', version, }; failures.push(failure); if (options.failFast && failures.length > 0) { break; } } } if (options.failFast && failures.length > 0) { return this.reportResult(failures, options); } const sortedFileVersions = [...fileVersions].sort((a, b) => this.fileService.compareVersions(a, b)); const sortedDbVersions = [...dbVersions].sort((a, b) => this.fileService.compareVersions(a, b)); for (let i = 0; i < Math.min(sortedFileVersions.length, sortedDbVersions.length); i++) { if (sortedFileVersions[i] !== sortedDbVersions[i]) { const failure = { message: `Migration order mismatch: expected ${sortedFileVersions[i]} but found ${sortedDbVersions[i]} at position ${i}`, severity: 'ERROR', type: 'ORDER_MISMATCH', version: sortedFileVersions[i], }; failures.push(failure); if (options.failFast) { break; } } } if (options.failFast && failures.length > 0) { return this.reportResult(failures, options); } for (const migration of appliedMigrations) { if (migration.node.version === 'BASELINE') continue; try { const { statements } = await this.fileService.prepareMigration(migration.node.source); const currentChecksum = (0, utils_1.generateChecksum)(statements); if (currentChecksum !== migration.node.checksum) { const failure = { message: `Checksum mismatch for ${migration.node.source}: file was modified after it was applied to the database`, severity: 'ERROR', type: 'CHECKSUM_MISMATCH', version: migration.node.version, }; failures.push(failure); if (options.failFast) { break; } } } catch (error) { const message = error instanceof Error ? error.message : String(error); const failure = { message: `Failed to validate checksum for ${migration.node.source}: ${message}`, severity: 'ERROR', type: 'OTHER', version: migration.node.version, }; failures.push(failure); if (options.failFast) { break; } } } return this.reportResult(failures); } catch (error) { logger_1.Logger.error(ValidateService.MESSAGES.VALIDATION_ERROR); throw this.wrapError(error); } } printValidationFailures(failures, summaryOnly = false) { logger_1.Logger.error(ValidateService.MESSAGES.VALIDATION_ERROR); const failuresByType = {}; for (const failure of failures) { if (!failuresByType[failure.type]) { failuresByType[failure.type] = []; } failuresByType[failure.type].push(failure); } logger_1.Logger.info((0, kleur_1.bold)('Validation Failure Summary:')); for (const [type, failures] of Object.entries(failuresByType)) { logger_1.Logger.info(`${(0, kleur_1.bold)(type)}: ${(0, kleur_1.red)(failures.length.toString())} issue(s)`); } const MAX_EXAMPLES = 3; for (const [type, typedFailures] of Object.entries(failuresByType)) { if (typedFailures.length > 0) { logger_1.Logger.info(`\n${type} failures found:`); for (const failure of typedFailures.slice(0, MAX_EXAMPLES)) { logger_1.Logger.info(` - ${failure.message}`); } if (typedFailures.length > MAX_EXAMPLES) { logger_1.Logger.info(` - ... and ${typedFailures.length - MAX_EXAMPLES} more similar issues`); } } } logger_1.Logger.info('\nSuggested actions:'); if (failuresByType.MISSING_FILE) { logger_1.Logger.info(' - For MISSING_FILE errors: Recover missing migration files from source control or backups'); } if (failuresByType.MISSING_DB) { logger_1.Logger.info(' - For MISSING_DB errors: Run "morpheus migrate" to apply pending migrations'); } if (failuresByType.CHECKSUM_MISMATCH) { logger_1.Logger.info(' - For CHECKSUM_MISMATCH errors: Restore original migration files or use "morpheus clean" to reset (use with caution)'); } if (summaryOnly || !logger_1.Logger.isDebugEnabled()) { logger_1.Logger.info('\nRun with --debug flag to see full details of all failures'); logger_1.Logger.info('Or use --output-file=path/to/file.json to export full results'); } if (logger_1.Logger.isDebugEnabled() && !summaryOnly) { logger_1.Logger.info('\nDetailed failure information:'); const failuresByType = {}; for (const failure of failures) { if (!failuresByType[failure.type]) { failuresByType[failure.type] = []; } failuresByType[failure.type].push({ message: failure.message, severity: failure.severity, version: failure.version || 'N/A', }); } const report = { failuresByType, timestamp: new Date().toISOString(), totalFailures: failures.length, }; console.log(JSON.stringify(report, null, 2)); } } reportResult(failures, options) { const result = { failures, isValid: failures.length === 0, }; if (result.isValid) { logger_1.Logger.info(ValidateService.MESSAGES.VALIDATION_SUCCESS); } else { this.printValidationFailures(failures, options?.summaryOnly); } return result; } wrapError(error) { const message = error instanceof Error ? error.message : String(error); return new errors_1.MigrationError(`Validation error: ${message}`); } } exports.ValidateService = ValidateService; //# sourceMappingURL=validate.service.js.map