UNPKG

easy-mongo-orm

Version:

A powerful and elegant MongoDB/Mongoose toolkit that makes database operations a breeze with built-in caching, search, pagination, performance monitoring, soft delete, versioning, data export/import, schema validation, and migration utilities

377 lines (357 loc) 10.9 kB
"use strict"; /** * Data Migration Utilities for Easy-Mongo * Provides tools for schema migrations and data transformations */ const mongoose = require('mongoose'); class MigrationManager { constructor(options = {}) { this.options = options; this.migrations = []; // Create migration tracking collection if it doesn't exist this._createMigrationModel(); } /** * Create migration model for tracking applied migrations * @private */ _createMigrationModel() { const migrationSchema = new mongoose.Schema({ name: { type: String, required: true, unique: true }, appliedAt: { type: Date, default: Date.now }, version: { type: Number, required: true }, description: { type: String }, duration: { type: Number } // in milliseconds }); // Check if model already exists if (mongoose.models.Migration) { this.MigrationModel = mongoose.model('Migration'); } else { this.MigrationModel = mongoose.model('Migration', migrationSchema); } } /** * Register a migration * @param {Object} migration - Migration object * @param {string} migration.name - Unique name for the migration * @param {number} migration.version - Migration version number * @param {string} migration.description - Description of what the migration does * @param {Function} migration.up - Function to apply the migration * @param {Function} migration.down - Function to revert the migration */ registerMigration(migration) { if (!migration.name || !migration.version || !migration.up) { throw new Error('Migration must have a name, version, and up function'); } // Check for duplicate names const existingMigration = this.migrations.find(m => m.name === migration.name); if (existingMigration) { throw new Error(`Migration with name "${migration.name}" already exists`); } this.migrations.push(migration); } /** * Apply all pending migrations * @param {Object} options - Migration options * @returns {Promise<Array>} - Applied migrations */ async applyMigrations(options = {}) { const appliedMigrations = await this.MigrationModel.find().sort({ version: 1 }); const appliedNames = appliedMigrations.map(m => m.name); // Get pending migrations const pendingMigrations = this.migrations.filter(m => !appliedNames.includes(m.name)).sort((a, b) => a.version - b.version); if (pendingMigrations.length === 0) { return { applied: 0, message: 'No pending migrations' }; } const results = []; // Apply each migration for (const migration of pendingMigrations) { const startTime = Date.now(); try { // Apply migration await migration.up(); // Record migration const duration = Date.now() - startTime; await this.MigrationModel.create({ name: migration.name, version: migration.version, description: migration.description, appliedAt: new Date(), duration }); results.push({ name: migration.name, version: migration.version, success: true, duration }); } catch (error) { results.push({ name: migration.name, version: migration.version, success: false, error: error.message }); if (!options.continueOnError) { throw new Error(`Migration "${migration.name}" failed: ${error.message}`); } } } return { applied: results.filter(r => r.success).length, failed: results.filter(r => !r.success).length, results }; } /** * Rollback the last applied migration * @returns {Promise<Object>} - Rollback result */ async rollbackLastMigration() { // Get the last applied migration const lastApplied = await this.MigrationModel.findOne().sort({ appliedAt: -1 }); if (!lastApplied) { return { rolled_back: 0, message: 'No migrations to rollback' }; } // Find the migration in our registered migrations const migration = this.migrations.find(m => m.name === lastApplied.name); if (!migration) { throw new Error(`Migration "${lastApplied.name}" not found in registered migrations`); } if (!migration.down) { throw new Error(`Migration "${lastApplied.name}" does not have a down function`); } const startTime = Date.now(); try { // Apply rollback await migration.down(); // Remove migration record await this.MigrationModel.deleteOne({ name: lastApplied.name }); const duration = Date.now() - startTime; return { rolled_back: 1, name: migration.name, version: migration.version, duration }; } catch (error) { throw new Error(`Rollback of "${migration.name}" failed: ${error.message}`); } } /** * Rollback to a specific version * @param {number} targetVersion - Version to rollback to * @returns {Promise<Object>} - Rollback result */ async rollbackToVersion(targetVersion) { // Get all applied migrations with version greater than target const migrationsToRollback = await this.MigrationModel.find({ version: { $gt: targetVersion } }).sort({ version: -1 }); if (migrationsToRollback.length === 0) { return { rolled_back: 0, message: 'No migrations to rollback' }; } const results = []; // Rollback each migration for (const appliedMigration of migrationsToRollback) { // Find the migration in our registered migrations const migration = this.migrations.find(m => m.name === appliedMigration.name); if (!migration) { throw new Error(`Migration "${appliedMigration.name}" not found in registered migrations`); } if (!migration.down) { throw new Error(`Migration "${appliedMigration.name}" does not have a down function`); } const startTime = Date.now(); try { // Apply rollback await migration.down(); // Remove migration record await this.MigrationModel.deleteOne({ name: appliedMigration.name }); const duration = Date.now() - startTime; results.push({ name: migration.name, version: migration.version, success: true, duration }); } catch (error) { results.push({ name: migration.name, version: migration.version, success: false, error: error.message }); throw new Error(`Rollback of "${migration.name}" failed: ${error.message}`); } } return { rolled_back: results.length, results }; } /** * Get migration status * @returns {Promise<Object>} - Migration status */ async getMigrationStatus() { const appliedMigrations = await this.MigrationModel.find().sort({ version: 1 }); const appliedNames = appliedMigrations.map(m => m.name); // Get pending migrations const pendingMigrations = this.migrations.filter(m => !appliedNames.includes(m.name)).sort((a, b) => a.version - b.version); return { applied: appliedMigrations.map(m => ({ name: m.name, version: m.version, appliedAt: m.appliedAt, description: m.description, duration: m.duration })), pending: pendingMigrations.map(m => ({ name: m.name, version: m.version, description: m.description })) }; } /** * Create a migration helper for common schema changes * @param {Object} model - Mongoose model * @returns {Object} - Migration helper */ createMigrationHelper(model) { return { /** * Add a field to all documents * @param {string} field - Field name * @param {any} defaultValue - Default value for the field * @returns {Promise<Object>} - Update result */ addField: async (field, defaultValue) => { const updateQuery = { $set: { [field]: defaultValue } }; return model.updateMany({}, updateQuery); }, /** * Remove a field from all documents * @param {string} field - Field name * @returns {Promise<Object>} - Update result */ removeField: async field => { const updateQuery = { $unset: { [field]: 1 } }; return model.updateMany({}, updateQuery); }, /** * Rename a field in all documents * @param {string} oldName - Old field name * @param {string} newName - New field name * @returns {Promise<Object>} - Update result */ renameField: async (oldName, newName) => { const updateQuery = { $rename: { [oldName]: newName } }; return model.updateMany({}, updateQuery); }, /** * Transform a field in all documents * @param {string} field - Field name * @param {Function} transformFn - Function to transform the field value * @returns {Promise<Object>} - Update result */ transformField: async (field, transformFn) => { const documents = await model.find({}); const bulkOps = []; for (const doc of documents) { if (doc[field] !== undefined) { const newValue = transformFn(doc[field], doc); bulkOps.push({ updateOne: { filter: { _id: doc._id }, update: { $set: { [field]: newValue } } } }); } } if (bulkOps.length === 0) { return { modifiedCount: 0 }; } return model.bulkWrite(bulkOps); }, /** * Copy data from one field to another * @param {string} sourceField - Source field name * @param {string} targetField - Target field name * @returns {Promise<Object>} - Update result */ copyField: async (sourceField, targetField) => { const aggregation = [{ $addFields: { [targetField]: `$${sourceField}` } }, { $merge: { into: model.collection.name, whenMatched: 'merge' } }]; return model.aggregate(aggregation); } }; } } module.exports = MigrationManager;