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
JavaScript
;
/* 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