data_migration_for_rails 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +17 -0
  3. data/README.md +196 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/manifest.js +2 -0
  6. data/app/assets/stylesheets/application.css +15 -0
  7. data/app/channels/application_cable/channel.rb +6 -0
  8. data/app/channels/application_cable/connection.rb +6 -0
  9. data/app/controllers/concerns/data_migration/pundit_authorization.rb +12 -0
  10. data/app/controllers/data_migration/application_controller.rb +63 -0
  11. data/app/controllers/data_migration/exports_controller.rb +68 -0
  12. data/app/controllers/data_migration/imports_controller.rb +78 -0
  13. data/app/controllers/data_migration/migration_executions_controller.rb +75 -0
  14. data/app/controllers/data_migration/migration_plans_controller.rb +103 -0
  15. data/app/controllers/data_migration/migration_steps_controller.rb +164 -0
  16. data/app/controllers/data_migration/users_controller.rb +71 -0
  17. data/app/controllers/users/sessions_controller.rb +30 -0
  18. data/app/helpers/data_migration/application_helper.rb +24 -0
  19. data/app/jobs/application_job.rb +9 -0
  20. data/app/jobs/export_job.rb +27 -0
  21. data/app/jobs/import_job.rb +28 -0
  22. data/app/mailers/application_mailer.rb +6 -0
  23. data/app/models/application_record.rb +5 -0
  24. data/app/models/data_migration_user.rb +43 -0
  25. data/app/models/migration_execution.rb +93 -0
  26. data/app/models/migration_plan.rb +23 -0
  27. data/app/models/migration_record.rb +60 -0
  28. data/app/models/migration_step.rb +150 -0
  29. data/app/policies/application_policy.rb +53 -0
  30. data/app/policies/data_migration/user_policy.rb +27 -0
  31. data/app/policies/data_migration_user_policy.rb +37 -0
  32. data/app/policies/migration_execution_policy.rb +33 -0
  33. data/app/policies/migration_plan_policy.rb +41 -0
  34. data/app/policies/migration_step_policy.rb +29 -0
  35. data/app/services/data_migration/model_registry.rb +95 -0
  36. data/app/services/exports/generator_service.rb +444 -0
  37. data/app/services/imports/processor_service.rb +457 -0
  38. data/app/services/migration_plans/export_config_service.rb +41 -0
  39. data/app/services/migration_plans/import_config_service.rb +158 -0
  40. data/app/views/data_migration/devise/registrations/edit.html.erb +41 -0
  41. data/app/views/data_migration/devise/sessions/new.html.erb +35 -0
  42. data/app/views/data_migration/devise/shared/_error_messages.html.erb +13 -0
  43. data/app/views/data_migration/devise/shared/_links.html.erb +21 -0
  44. data/app/views/data_migration/exports/new.html.erb +85 -0
  45. data/app/views/data_migration/imports/new.html.erb +70 -0
  46. data/app/views/data_migration/migration_executions/index.html.erb +78 -0
  47. data/app/views/data_migration/migration_executions/show.html.erb +338 -0
  48. data/app/views/data_migration/migration_plans/_form.html.erb +28 -0
  49. data/app/views/data_migration/migration_plans/edit.html.erb +12 -0
  50. data/app/views/data_migration/migration_plans/index.html.erb +118 -0
  51. data/app/views/data_migration/migration_plans/new.html.erb +9 -0
  52. data/app/views/data_migration/migration_plans/show.html.erb +105 -0
  53. data/app/views/data_migration/migration_steps/_form.html.erb +473 -0
  54. data/app/views/data_migration/migration_steps/edit.html.erb +12 -0
  55. data/app/views/data_migration/migration_steps/new.html.erb +9 -0
  56. data/app/views/data_migration/users/_form.html.erb +49 -0
  57. data/app/views/data_migration/users/edit.html.erb +2 -0
  58. data/app/views/data_migration/users/index.html.erb +41 -0
  59. data/app/views/data_migration/users/new.html.erb +2 -0
  60. data/app/views/data_migration/users/show.html.erb +133 -0
  61. data/app/views/layouts/_navbar.html.erb +38 -0
  62. data/app/views/layouts/data_migration.html.erb +37 -0
  63. data/app/views/layouts/mailer.html.erb +13 -0
  64. data/app/views/layouts/mailer.text.erb +1 -0
  65. data/app/views/users/registrations/edit.html.erb +41 -0
  66. data/app/views/users/sessions/new.html.erb +35 -0
  67. data/app/views/users/shared/_error_messages.html.erb +13 -0
  68. data/app/views/users/shared/_links.html.erb +21 -0
  69. data/config/initializers/assets.rb +14 -0
  70. data/config/initializers/content_security_policy.rb +27 -0
  71. data/config/initializers/devise.rb +313 -0
  72. data/config/initializers/filter_parameter_logging.rb +10 -0
  73. data/config/initializers/inflections.rb +18 -0
  74. data/config/initializers/permissions_policy.rb +15 -0
  75. data/config/initializers/warden.rb +14 -0
  76. data/config/locales/devise.en.yml +65 -0
  77. data/config/locales/en.yml +31 -0
  78. data/config/routes.rb +62 -0
  79. data/db/migrate/20251102121659_create_migration_plans.rb +13 -0
  80. data/db/migrate/20251102122012_create_migration_steps.rb +24 -0
  81. data/db/migrate/20251105215702_create_migration_executions.rb +23 -0
  82. data/db/migrate/20251105215853_create_migration_records.rb +16 -0
  83. data/db/migrate/20251115154000_remove_unused_attributes.rb +17 -0
  84. data/db/migrate/20251116120000_add_filter_params_to_migration_executions.rb +7 -0
  85. data/db/migrate/20251118140000_create_data_migration_users.rb +27 -0
  86. data/db/migrate/20251118200641_add_user_foreign_keys.rb +15 -0
  87. data/db/migrate/20251124140000_add_attachment_export_mode_to_migration_steps.rb +9 -0
  88. data/db/schema.rb +102 -0
  89. data/db/seeds.rb +19 -0
  90. data/lib/data_migration/engine.rb +28 -0
  91. data/lib/data_migration/version.rb +5 -0
  92. data/lib/data_migration.rb +8 -0
  93. data/lib/tasks/data_migration_tasks.rake +40 -0
  94. metadata +279 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 49026939624c298b1cb497970e5768eb0c8494c2803e601736c7df95c5a9cec1
4
+ data.tar.gz: a8f16d45bfef5ce2e926bae3e558d33e0167b99640904c5b66a1e5731810e87b
5
+ SHA512:
6
+ metadata.gz: 87560a148c1981e1783c68848e9919314fd199e46868960e308e6f3ed2fab53e7d48fa94e0db9712a3218f9ba18c42f1bc3cbb134dbcd86568df6322b732ab39
7
+ data.tar.gz: 91057070c7092b2d766f5f4f2a0bda8b5e88db334108bd5ab989cecebc6cf4f71e84889c9d59f9ce4a2543e948251e57b65a1f1d934e0b848ade659713421cda
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2025 Vaibhav Rokkam
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # Data Migration - Rails Engine
2
+
3
+ A mountable Rails engine for migrating data between Rails application environments with full audit trails, role-based access control, and dependency management.
4
+
5
+ ## Features
6
+
7
+ - **Role-Based Access Control** - Admin, Operator, and Viewer roles
8
+ - **Migration Plans** - Reusable migration configurations with execution order
9
+ - **Plan Configuration Import/Export** - Share migration plans across environments as JSON
10
+ - **Dynamic Filtering** - ActiveRecord queries with runtime parameter substitution
11
+ - **Dependency Management** - Maintain referential integrity across related models
12
+ - **Association Handling** - Remap foreign keys and handle polymorphic associations
13
+ - **Attachment Handling** - Export/import Active Storage attachments with URL or raw data modes
14
+ - **Background Processing** - Sidekiq-based async exports and imports
15
+ - **Audit Trail** - Complete execution history and detailed record-level tracking
16
+ - **Real-time Progress** - ActionCable integration for live updates
17
+
18
+ ![Landing Page](screenshots/landing_page.png)
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ### 1. Add to Gemfile
25
+
26
+ ```ruby
27
+ gem "data_migration_for_rails", "~> 0.1.1"
28
+ ```
29
+
30
+ ### 2. Install and Migrate
31
+
32
+ ```bash
33
+ bundle install
34
+ bundle exec rake data_migration_engine:install:migrations
35
+ bundle exec rake db:migrate
36
+ ```
37
+
38
+ ### 3. Mount Routes
39
+
40
+ In `config/routes.rb`:
41
+
42
+ ```ruby
43
+ Rails.application.routes.draw do
44
+ mount DataMigration::Engine, at: "/data_migration"
45
+ end
46
+ ```
47
+
48
+ ### 4. Seed Initial Admin User
49
+
50
+ ```bash
51
+ bundle exec rake data_migration:seed
52
+ ```
53
+
54
+ This creates an admin user with independent authentication:
55
+ - **Email:** admin@datamigration.local
56
+ - **Password:** password
57
+
58
+ **Change the password immediately** after first login! Admin users can create additional users via the "Users" menu.
59
+
60
+ ### 5. Configure Sidekiq
61
+
62
+ In `config/application.rb`:
63
+
64
+ ```ruby
65
+ config.active_job.queue_adapter = :sidekiq
66
+ ```
67
+
68
+ Start services:
69
+ ```bash
70
+ redis-server
71
+ bundle exec sidekiq
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Prerequisites
77
+
78
+ - Ruby 3.0+
79
+ - Rails 7.0+
80
+ - Redis
81
+ - Devise (for authentication)
82
+ - Pundit (for authorization)
83
+ - Active Storage (optional, for attachment handling)
84
+
85
+ ---
86
+
87
+ ## Usage
88
+
89
+ ### 1. Create Migration Plan
90
+
91
+ Navigate to `/data_migration/migration_plans` and create a new plan.
92
+
93
+ ![Migration Plan Page](screenshots/migration_plan_page.png)
94
+
95
+ ### 2. Add Migration Steps
96
+
97
+ Configure each step with:
98
+
99
+ - **Model Name**: Select from dropdown (shows fields and attachments)
100
+ - **Sequence**: Execution order
101
+ - **Filter Query** (optional): ActiveRecord query with optional placeholders
102
+
103
+ Examples:
104
+ ```ruby
105
+ where(active: true).limit(100)
106
+ where("created_at > ?", "{{cutoff_date}}")
107
+ ```
108
+
109
+ - **Column Overrides**: Export association attributes
110
+ - **Association ID Mappings**: Remap foreign keys on import
111
+ - **Dependee Attribute Mapping**: Filter based on parent step's exported records
112
+ - **Attachment Export Mode**: Choose how to handle Active Storage attachments:
113
+ - **Ignore** - Skip attachments
114
+ - **URL** - Export attachment URLs (for cloud storage)
115
+ - **Raw Data** - Export actual files in archive (for local storage)
116
+
117
+ ![Migration Step Form](screenshots/migration_step_form.png)
118
+
119
+ ### 3. Export
120
+
121
+ Click **Export**, fill in any filter parameters, and download the `.tar.gz` archive.
122
+
123
+ ![Export Execution Form](screenshots/export_execution_form.png)
124
+
125
+ ![Executed Export Page](screenshots/executed_export_page.png)
126
+
127
+ ### 4. Import
128
+
129
+ On target environment, click **Import**, upload the archive, and select conflict resolution strategy.
130
+
131
+ ![Import Execution Form](screenshots/import_execution_form.png)
132
+
133
+ ![Executed Import Page](screenshots/executed_import_page.png)
134
+
135
+ ---
136
+
137
+ ## Advanced Features
138
+
139
+ ### Dynamic Filter Parameters
140
+
141
+ Use `{{placeholder}}` syntax in filter queries. Values are collected at export time and validated before execution.
142
+
143
+ ### Referential Integrity
144
+
145
+ Set **Depends On Step** and **Dependee Attribute Mapping** to ensure child records only include those referencing exported parent records.
146
+
147
+ Example: Export only employees whose companies were exported.
148
+
149
+ ### Polymorphic Associations
150
+
151
+ Configure polymorphic associations with type-specific lookup attributes for proper ID remapping on import.
152
+
153
+ ### Plan Configuration Import/Export
154
+
155
+ Share migration plan configurations across environments:
156
+
157
+ 1. **Export**: Click "📋 Export Config" on any plan → Download JSON file
158
+ 2. **Import**: Click "📋 Import Config" → Upload JSON → Plan and steps created/updated automatically
159
+
160
+ Perfect for deploying migration plans to production or sharing with team members.
161
+
162
+ ---
163
+
164
+ ## Security
165
+
166
+ - Pundit-based authorization on all actions
167
+ - Strong parameter validation
168
+ - Safe ActiveRecord query evaluation
169
+ - Multi-layer input validation
170
+ - Complete audit trail
171
+
172
+ ---
173
+
174
+ ## Troubleshooting
175
+
176
+ **Sidekiq not processing:**
177
+ - Check Redis: `redis-cli ping`
178
+ - Verify `config.active_job.queue_adapter = :sidekiq`
179
+ - Check logs: `tail -f log/sidekiq.log`
180
+
181
+ **Filter parameter errors:**
182
+ - Ensure placeholders are inside quotes: `"{{param}}"` not `{{param}}`
183
+ - Provide all required parameter values before export
184
+
185
+ **Model not found:**
186
+ - Verify exact ActiveRecord class name (case-sensitive, singular form)
187
+ - Example: Use "Company" not "company" or "Companies"
188
+ - Check lookup attributes exist on target models
189
+
190
+ ---
191
+
192
+ ## License
193
+
194
+ This project is licensed under the [GNU General Public License v3.0](LICENSE).
195
+
196
+ Copyright © 2025 Vaibhav Rokkam.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
4
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5
+
6
+ require_relative 'config/application'
7
+
8
+ Rails.application.load_tasks
@@ -0,0 +1,2 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
6
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Channel < ActionCable::Channel::Base
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Connection < ActionCable::Connection::Base
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataMigration
4
+ module PunditAuthorization
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_action :verify_authorized, except: :index
9
+ after_action :verify_policy_scoped, only: :index
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataMigration
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :null_session, prepend: true
6
+
7
+ # Devise authentication
8
+ before_action :authenticate_user!, unless: :devise_controller?
9
+
10
+ # Pundit authorization
11
+ include Pundit::Authorization
12
+
13
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
14
+
15
+ # Layout
16
+ layout 'data_migration'
17
+
18
+ # Make engine route helpers available in views
19
+ helper DataMigration::Engine.routes.url_helpers
20
+
21
+ # Make model registry available to all views
22
+ helper_method :model_registry
23
+
24
+ private
25
+
26
+ def model_registry
27
+ @model_registry ||= DataMigration::ModelRegistry.all_models
28
+ end
29
+
30
+ def user_not_authorized
31
+ flash[:alert] = 'You are not authorized to perform this action.'
32
+ redirect_to(request.referrer || '/data_migration/migration_plans')
33
+ end
34
+
35
+ # Devise helpers are automatically available as:
36
+ # - current_user (from devise_for :users)
37
+ # - user_signed_in? (from devise_for :users)
38
+ # No need to override since resource name is :users
39
+
40
+ # Override Devise redirect after sign in
41
+ def after_sign_in_path_for(_resource)
42
+ '/data_migration/migration_plans'
43
+ end
44
+
45
+ # Handle CSRF token verification failures for Devise controllers
46
+ # This prevents errors when Devise's FailureApp redirects after failed login
47
+ def handle_unverified_request
48
+ if devise_controller?
49
+ if controller_name == 'sessions'
50
+ # For login forms, redirect back to login to get fresh token
51
+ flash[:alert] = 'Your session has expired. Please try signing in again.'
52
+ else
53
+ # For other Devise controllers, sign out and redirect
54
+ sign_out if user_signed_in?
55
+ flash[:alert] = 'Session expired. Please sign in again.'
56
+ end
57
+ redirect_to new_session_path(:user)
58
+ else
59
+ super
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataMigration
4
+ class ExportsController < ApplicationController
5
+ before_action :set_migration_plan
6
+
7
+ def new
8
+ authorize @migration_plan, :execute?
9
+ @filter_params = extract_placeholders_from_plan
10
+ end
11
+
12
+ def create
13
+ authorize @migration_plan, :execute?
14
+
15
+ # Validate that all required filter parameters are provided
16
+ required_params = extract_placeholders_from_plan
17
+
18
+ # Permit the filter_params dynamically based on what's required
19
+ provided_params = if params[:filter_params].present? && required_params.any?
20
+ params.require(:filter_params).permit(*required_params.keys)
21
+ else
22
+ {}
23
+ end
24
+
25
+ missing_params = required_params.keys.select do |param|
26
+ provided_params[param].blank?
27
+ end
28
+
29
+ if missing_params.any?
30
+ flash[:alert] = "Please provide values for: #{missing_params.join(', ')}"
31
+ @filter_params = required_params
32
+ render :new and return
33
+ end
34
+
35
+ execution = MigrationExecution.create!(
36
+ migration_plan: @migration_plan,
37
+ user: current_user,
38
+ execution_type: :export,
39
+ status: :pending,
40
+ filter_params: provided_params.to_h
41
+ )
42
+
43
+ ExportJob.perform_later(execution.id)
44
+
45
+ redirect_to "/data_migration/migration_executions/#{execution.id}",
46
+ notice: 'Export started. You will be notified when it completes.'
47
+ end
48
+
49
+ private
50
+
51
+ def set_migration_plan
52
+ @migration_plan = MigrationPlan.find(params[:migration_plan_id])
53
+ end
54
+
55
+ def extract_placeholders_from_plan
56
+ placeholders = {}
57
+ @migration_plan.migration_steps.each do |step|
58
+ next if step.filter_query.blank?
59
+
60
+ # Extract placeholders like {{param_name}} from filter query
61
+ step.filter_query.scan(/\{\{(\w+)\}\}/).flatten.each do |param|
62
+ placeholders[param] = nil
63
+ end
64
+ end
65
+ placeholders
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataMigration
4
+ class ImportsController < ApplicationController
5
+ before_action :set_migration_plan
6
+
7
+ def new
8
+ authorize @migration_plan, :execute?
9
+ @filter_params = extract_placeholders_from_plan
10
+ @last_export = @migration_plan.migration_executions.export.completed.order(created_at: :desc).first
11
+ end
12
+
13
+ def create
14
+ authorize @migration_plan, :execute?
15
+
16
+ unless params[:archive_file].present?
17
+ redirect_to "/data_migration/migration_plans/#{@migration_plan.id}/import/new",
18
+ alert: 'Please select a file to upload.'
19
+ return
20
+ end
21
+
22
+ uploaded_file = params[:archive_file]
23
+
24
+ # Save uploaded file to tmp directory
25
+ file_path = save_uploaded_file(uploaded_file)
26
+
27
+ # Use provided filter params or default to empty hash
28
+ filter_params = params[:filter_params].present? ? params[:filter_params] : {}
29
+
30
+ execution = MigrationExecution.create!(
31
+ migration_plan: @migration_plan,
32
+ user: current_user,
33
+ execution_type: :import,
34
+ status: :pending,
35
+ file_path: file_path,
36
+ filter_params: filter_params
37
+ )
38
+
39
+ ImportJob.perform_later(execution.id)
40
+
41
+ redirect_to "/data_migration/migration_executions/#{execution.id}",
42
+ notice: 'Import started. You will be notified when it completes.'
43
+ end
44
+
45
+ private
46
+
47
+ def set_migration_plan
48
+ @migration_plan = MigrationPlan.find(params[:migration_plan_id])
49
+ end
50
+
51
+ def extract_placeholders_from_plan
52
+ placeholders = {}
53
+ @migration_plan.migration_steps.each do |step|
54
+ next if step.filter_query.blank?
55
+
56
+ # Extract placeholders like {{param_name}} from filter query
57
+ step.filter_query.scan(/\{\{(\w+)\}\}/).flatten.each do |param|
58
+ placeholders[param] = nil
59
+ end
60
+ end
61
+ placeholders
62
+ end
63
+
64
+ def save_uploaded_file(uploaded_file)
65
+ timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
66
+ filename = "import_#{timestamp}_#{uploaded_file.original_filename}"
67
+ file_path = Rails.root.join('tmp', 'imports', filename)
68
+
69
+ FileUtils.mkdir_p(File.dirname(file_path))
70
+
71
+ File.open(file_path, 'wb') do |file|
72
+ file.write(uploaded_file.read)
73
+ end
74
+
75
+ file_path.to_s
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataMigration
4
+ class MigrationExecutionsController < ApplicationController
5
+ include DataMigration::PunditAuthorization
6
+
7
+ before_action :set_execution, only: %i[show download]
8
+
9
+ def index
10
+ @executions = policy_scope(MigrationExecution).recent.includes(:migration_plan, :user)
11
+ end
12
+
13
+ def show
14
+ authorize @execution
15
+
16
+ # Initialize variables
17
+ @migration_records = []
18
+ @model_names = []
19
+ @action_counts = {}
20
+
21
+ # Only load migration records for import executions
22
+ return unless @execution.import?
23
+
24
+ # Reload association to ensure fresh data
25
+ @execution.reload
26
+
27
+ # Filter and paginate migration records
28
+ @migration_records = @execution.migration_records.order(created_at: :desc)
29
+
30
+ # Apply filters if provided
31
+ @migration_records = @migration_records.by_model(params[:model]) if params[:model].present?
32
+
33
+ @migration_records = @migration_records.by_action(params[:filter_action]) if params[:filter_action].present?
34
+
35
+ # Limit to 500 records for display (can be customized)
36
+ @limit = (params[:limit] || 500).to_i
37
+ @migration_records = @migration_records.limit(@limit).to_a
38
+
39
+ # Get unique model names for filter dropdown
40
+ @model_names = @execution.migration_records.distinct.pluck(:migrated_model_name).sort
41
+
42
+ # Get total counts by action (convert enum integers to string keys)
43
+ raw_counts = @execution.migration_records.group(:action).count
44
+ raw_counts.each do |action_value, count|
45
+ action_name = MigrationRecord.actions.key(action_value)
46
+ @action_counts[action_name] = count if action_name
47
+ end
48
+ end
49
+
50
+ def download
51
+ authorize @execution, :download?
52
+
53
+ unless @execution.completed? && @execution.export? && @execution.file_path.present?
54
+ redirect_to @execution, alert: 'Export file not available.'
55
+ return
56
+ end
57
+
58
+ unless File.exist?(@execution.file_path)
59
+ redirect_to @execution, alert: 'Export file not found.'
60
+ return
61
+ end
62
+
63
+ send_file @execution.file_path,
64
+ filename: File.basename(@execution.file_path),
65
+ type: 'application/gzip',
66
+ disposition: 'attachment'
67
+ end
68
+
69
+ private
70
+
71
+ def set_execution
72
+ @execution = MigrationExecution.find(params[:id])
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataMigration
4
+ class MigrationPlansController < ApplicationController
5
+ include DataMigration::PunditAuthorization
6
+
7
+ before_action :set_migration_plan, only: %i[show edit update destroy export_config]
8
+
9
+ def index
10
+ @migration_plans = policy_scope(MigrationPlan).ordered_by_name
11
+ end
12
+
13
+ def show
14
+ authorize @migration_plan
15
+ end
16
+
17
+ def new
18
+ @migration_plan = MigrationPlan.new
19
+ authorize @migration_plan
20
+ end
21
+
22
+ def create
23
+ @migration_plan = MigrationPlan.new(migration_plan_params)
24
+ authorize @migration_plan
25
+
26
+ if @migration_plan.save
27
+ redirect_to "/data_migration/migration_plans/#{@migration_plan.id}",
28
+ notice: 'Migration plan was successfully created.'
29
+ else
30
+ render :new, status: :unprocessable_entity
31
+ end
32
+ end
33
+
34
+ def edit
35
+ authorize @migration_plan
36
+ end
37
+
38
+ def update
39
+ authorize @migration_plan
40
+
41
+ if @migration_plan.update(migration_plan_params)
42
+ redirect_to "/data_migration/migration_plans/#{@migration_plan.id}",
43
+ notice: 'Migration plan was successfully updated.'
44
+ else
45
+ render :edit, status: :unprocessable_entity
46
+ end
47
+ end
48
+
49
+ def destroy
50
+ authorize @migration_plan
51
+
52
+ if @migration_plan.destroy
53
+ redirect_to '/data_migration/migration_plans', notice: 'Migration plan was successfully destroyed.'
54
+ else
55
+ redirect_to "/data_migration/migration_plans/#{@migration_plan.id}",
56
+ alert: 'Cannot delete migration plan with existing executions.'
57
+ end
58
+ end
59
+
60
+ def export_config
61
+ authorize @migration_plan
62
+
63
+ service = MigrationPlans::ExportConfigService.new(@migration_plan)
64
+ json_config = service.call
65
+
66
+ send_data json_config,
67
+ filename: "#{@migration_plan.name.parameterize}_config.json",
68
+ type: 'application/json',
69
+ disposition: 'attachment'
70
+ end
71
+
72
+ def import_config
73
+ authorize MigrationPlan.new
74
+
75
+ unless params[:config_file].present?
76
+ redirect_to '/data_migration/migration_plans', alert: 'Please select a configuration file to import.'
77
+ return
78
+ end
79
+
80
+ config_json = params[:config_file].read
81
+ service = MigrationPlans::ImportConfigService.new(config_json, current_user)
82
+
83
+ imported_plan = service.call
84
+
85
+ if service.success? && imported_plan
86
+ redirect_to "/data_migration/migration_plans/#{imported_plan.id}",
87
+ notice: 'Migration plan configuration imported successfully!'
88
+ else
89
+ redirect_to '/data_migration/migration_plans', alert: "Import failed: #{service.errors.join(', ')}"
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def set_migration_plan
96
+ @migration_plan = MigrationPlan.find(params[:id])
97
+ end
98
+
99
+ def migration_plan_params
100
+ params.require(:migration_plan).permit(:name, :description, settings: [])
101
+ end
102
+ end
103
+ end