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.
- checksums.yaml +7 -0
- data/LICENSE +17 -0
- data/README.md +196 -0
- data/Rakefile +8 -0
- data/app/assets/config/manifest.js +2 -0
- data/app/assets/stylesheets/application.css +15 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/controllers/concerns/data_migration/pundit_authorization.rb +12 -0
- data/app/controllers/data_migration/application_controller.rb +63 -0
- data/app/controllers/data_migration/exports_controller.rb +68 -0
- data/app/controllers/data_migration/imports_controller.rb +78 -0
- data/app/controllers/data_migration/migration_executions_controller.rb +75 -0
- data/app/controllers/data_migration/migration_plans_controller.rb +103 -0
- data/app/controllers/data_migration/migration_steps_controller.rb +164 -0
- data/app/controllers/data_migration/users_controller.rb +71 -0
- data/app/controllers/users/sessions_controller.rb +30 -0
- data/app/helpers/data_migration/application_helper.rb +24 -0
- data/app/jobs/application_job.rb +9 -0
- data/app/jobs/export_job.rb +27 -0
- data/app/jobs/import_job.rb +28 -0
- data/app/mailers/application_mailer.rb +6 -0
- data/app/models/application_record.rb +5 -0
- data/app/models/data_migration_user.rb +43 -0
- data/app/models/migration_execution.rb +93 -0
- data/app/models/migration_plan.rb +23 -0
- data/app/models/migration_record.rb +60 -0
- data/app/models/migration_step.rb +150 -0
- data/app/policies/application_policy.rb +53 -0
- data/app/policies/data_migration/user_policy.rb +27 -0
- data/app/policies/data_migration_user_policy.rb +37 -0
- data/app/policies/migration_execution_policy.rb +33 -0
- data/app/policies/migration_plan_policy.rb +41 -0
- data/app/policies/migration_step_policy.rb +29 -0
- data/app/services/data_migration/model_registry.rb +95 -0
- data/app/services/exports/generator_service.rb +444 -0
- data/app/services/imports/processor_service.rb +457 -0
- data/app/services/migration_plans/export_config_service.rb +41 -0
- data/app/services/migration_plans/import_config_service.rb +158 -0
- data/app/views/data_migration/devise/registrations/edit.html.erb +41 -0
- data/app/views/data_migration/devise/sessions/new.html.erb +35 -0
- data/app/views/data_migration/devise/shared/_error_messages.html.erb +13 -0
- data/app/views/data_migration/devise/shared/_links.html.erb +21 -0
- data/app/views/data_migration/exports/new.html.erb +85 -0
- data/app/views/data_migration/imports/new.html.erb +70 -0
- data/app/views/data_migration/migration_executions/index.html.erb +78 -0
- data/app/views/data_migration/migration_executions/show.html.erb +338 -0
- data/app/views/data_migration/migration_plans/_form.html.erb +28 -0
- data/app/views/data_migration/migration_plans/edit.html.erb +12 -0
- data/app/views/data_migration/migration_plans/index.html.erb +118 -0
- data/app/views/data_migration/migration_plans/new.html.erb +9 -0
- data/app/views/data_migration/migration_plans/show.html.erb +105 -0
- data/app/views/data_migration/migration_steps/_form.html.erb +473 -0
- data/app/views/data_migration/migration_steps/edit.html.erb +12 -0
- data/app/views/data_migration/migration_steps/new.html.erb +9 -0
- data/app/views/data_migration/users/_form.html.erb +49 -0
- data/app/views/data_migration/users/edit.html.erb +2 -0
- data/app/views/data_migration/users/index.html.erb +41 -0
- data/app/views/data_migration/users/new.html.erb +2 -0
- data/app/views/data_migration/users/show.html.erb +133 -0
- data/app/views/layouts/_navbar.html.erb +38 -0
- data/app/views/layouts/data_migration.html.erb +37 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/users/registrations/edit.html.erb +41 -0
- data/app/views/users/sessions/new.html.erb +35 -0
- data/app/views/users/shared/_error_messages.html.erb +13 -0
- data/app/views/users/shared/_links.html.erb +21 -0
- data/config/initializers/assets.rb +14 -0
- data/config/initializers/content_security_policy.rb +27 -0
- data/config/initializers/devise.rb +313 -0
- data/config/initializers/filter_parameter_logging.rb +10 -0
- data/config/initializers/inflections.rb +18 -0
- data/config/initializers/permissions_policy.rb +15 -0
- data/config/initializers/warden.rb +14 -0
- data/config/locales/devise.en.yml +65 -0
- data/config/locales/en.yml +31 -0
- data/config/routes.rb +62 -0
- data/db/migrate/20251102121659_create_migration_plans.rb +13 -0
- data/db/migrate/20251102122012_create_migration_steps.rb +24 -0
- data/db/migrate/20251105215702_create_migration_executions.rb +23 -0
- data/db/migrate/20251105215853_create_migration_records.rb +16 -0
- data/db/migrate/20251115154000_remove_unused_attributes.rb +17 -0
- data/db/migrate/20251116120000_add_filter_params_to_migration_executions.rb +7 -0
- data/db/migrate/20251118140000_create_data_migration_users.rb +27 -0
- data/db/migrate/20251118200641_add_user_foreign_keys.rb +15 -0
- data/db/migrate/20251124140000_add_attachment_export_mode_to_migration_steps.rb +9 -0
- data/db/schema.rb +102 -0
- data/db/seeds.rb +19 -0
- data/lib/data_migration/engine.rb +28 -0
- data/lib/data_migration/version.rb +5 -0
- data/lib/data_migration.rb +8 -0
- data/lib/tasks/data_migration_tasks.rake +40 -0
- 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
118
|
+
|
|
119
|
+
### 3. Export
|
|
120
|
+
|
|
121
|
+
Click **Export**, fill in any filter parameters, and download the `.tar.gz` archive.
|
|
122
|
+
|
|
123
|
+

|
|
124
|
+
|
|
125
|
+

|
|
126
|
+
|
|
127
|
+
### 4. Import
|
|
128
|
+
|
|
129
|
+
On target environment, click **Import**, upload the archive, and select conflict resolution strategy.
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+

|
|
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,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,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
|