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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MigrationStep < ApplicationRecord
|
|
4
|
+
# Associations
|
|
5
|
+
belongs_to :migration_plan
|
|
6
|
+
belongs_to :dependee, class_name: 'MigrationStep', optional: true
|
|
7
|
+
has_many :dependents, class_name: 'MigrationStep', foreign_key: :dependee_id
|
|
8
|
+
|
|
9
|
+
# Enums
|
|
10
|
+
enum attachment_export_mode: {
|
|
11
|
+
ignore: 0,
|
|
12
|
+
url: 1,
|
|
13
|
+
raw_data: 2
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Validations
|
|
17
|
+
validates :source_model_name, presence: true
|
|
18
|
+
validates :sequence, presence: true, numericality: { only_integer: true }
|
|
19
|
+
validate :validate_json_field_types
|
|
20
|
+
|
|
21
|
+
# Callbacks to set defaults
|
|
22
|
+
after_initialize :set_defaults, if: :new_record?
|
|
23
|
+
before_validation :parse_json_fields
|
|
24
|
+
|
|
25
|
+
# Scopes
|
|
26
|
+
scope :ordered_by_sequence, -> { order(:sequence) }
|
|
27
|
+
|
|
28
|
+
# Custom getter for dependee_attribute_mapping
|
|
29
|
+
def dependee_attribute_mapping
|
|
30
|
+
value = read_attribute(:dependee_attribute_mapping)
|
|
31
|
+
return {} if value.blank?
|
|
32
|
+
return value if value.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
JSON.parse(value)
|
|
35
|
+
rescue JSON::ParserError => e
|
|
36
|
+
Rails.logger.error "Failed to parse dependee_attribute_mapping for MigrationStep #{id}: #{e.message}"
|
|
37
|
+
{}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Custom setter for dependee_attribute_mapping
|
|
41
|
+
def dependee_attribute_mapping=(value)
|
|
42
|
+
if value.is_a?(String)
|
|
43
|
+
write_attribute(:dependee_attribute_mapping, value)
|
|
44
|
+
elsif value.is_a?(Hash)
|
|
45
|
+
write_attribute(:dependee_attribute_mapping, value.to_json)
|
|
46
|
+
else
|
|
47
|
+
write_attribute(:dependee_attribute_mapping, {}.to_json)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Custom getter for column_overrides
|
|
52
|
+
def column_overrides
|
|
53
|
+
value = read_attribute(:column_overrides)
|
|
54
|
+
return {} if value.blank?
|
|
55
|
+
return value if value.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
JSON.parse(value)
|
|
58
|
+
rescue JSON::ParserError => e
|
|
59
|
+
Rails.logger.error "Failed to parse column_overrides for MigrationStep #{id}: #{e.message}"
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Custom setter for column_overrides
|
|
64
|
+
def column_overrides=(value)
|
|
65
|
+
if value.is_a?(String)
|
|
66
|
+
write_attribute(:column_overrides, value)
|
|
67
|
+
elsif value.is_a?(Hash)
|
|
68
|
+
write_attribute(:column_overrides, value.to_json)
|
|
69
|
+
else
|
|
70
|
+
write_attribute(:column_overrides, {}.to_json)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Custom getter for association_overrides
|
|
75
|
+
def association_overrides
|
|
76
|
+
value = read_attribute(:association_overrides)
|
|
77
|
+
return {} if value.blank?
|
|
78
|
+
return value if value.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
JSON.parse(value)
|
|
81
|
+
rescue JSON::ParserError => e
|
|
82
|
+
Rails.logger.error "Failed to parse association_overrides for MigrationStep #{id}: #{e.message}"
|
|
83
|
+
{}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Custom setter for association_overrides
|
|
87
|
+
def association_overrides=(value)
|
|
88
|
+
if value.is_a?(String)
|
|
89
|
+
write_attribute(:association_overrides, value)
|
|
90
|
+
elsif value.is_a?(Hash)
|
|
91
|
+
write_attribute(:association_overrides, value.to_json)
|
|
92
|
+
else
|
|
93
|
+
write_attribute(:association_overrides, {}.to_json)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def set_defaults
|
|
100
|
+
self.dependee_attribute_mapping ||= {}
|
|
101
|
+
self.column_overrides ||= {}
|
|
102
|
+
self.association_overrides ||= {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_json_fields
|
|
106
|
+
# Parse JSON string fields into proper objects
|
|
107
|
+
json_fields = %i[
|
|
108
|
+
dependee_attribute_mapping
|
|
109
|
+
column_overrides
|
|
110
|
+
association_overrides
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
json_fields.each do |field|
|
|
114
|
+
value = send(field)
|
|
115
|
+
next if value.blank?
|
|
116
|
+
|
|
117
|
+
# If it's a string, try to parse it as JSON
|
|
118
|
+
next unless value.is_a?(String)
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
parsed_value = JSON.parse(value)
|
|
122
|
+
send("#{field}=", parsed_value)
|
|
123
|
+
rescue JSON::ParserError => e
|
|
124
|
+
# Add validation error for invalid JSON
|
|
125
|
+
errors.add(field, "contains invalid JSON: #{e.message}")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_json_field_types
|
|
131
|
+
# Ensure JSON fields are objects (Hash), not arrays
|
|
132
|
+
json_fields = {
|
|
133
|
+
dependee_attribute_mapping: 'Dependee Attribute Mapping',
|
|
134
|
+
column_overrides: 'Column Overrides',
|
|
135
|
+
association_overrides: 'Association ID Mappings'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
json_fields.each do |field, label|
|
|
139
|
+
value = send(field)
|
|
140
|
+
next if value.blank? || value == {}
|
|
141
|
+
|
|
142
|
+
# Check if it's a Hash (JSON object)
|
|
143
|
+
if value.is_a?(Array)
|
|
144
|
+
errors.add(field, "#{label} must be a JSON object {}, not an array []")
|
|
145
|
+
elsif !value.is_a?(Hash)
|
|
146
|
+
errors.add(field, "#{label} must be a valid JSON object")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ApplicationPolicy
|
|
4
|
+
attr_reader :user, :record
|
|
5
|
+
|
|
6
|
+
def initialize(user, record)
|
|
7
|
+
@user = user
|
|
8
|
+
@record = record
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def index?
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show?
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create?
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def new?
|
|
24
|
+
create?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def edit?
|
|
32
|
+
update?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy?
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Scope
|
|
40
|
+
def initialize(user, scope)
|
|
41
|
+
@user = user
|
|
42
|
+
@scope = scope
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resolve
|
|
46
|
+
raise NoMethodError, "You must define #resolve in #{self.class}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :user, :scope
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataMigration
|
|
4
|
+
class UserPolicy < ApplicationPolicy
|
|
5
|
+
class Scope < Scope
|
|
6
|
+
def resolve
|
|
7
|
+
scope.all
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def index?
|
|
12
|
+
user.admin?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create?
|
|
16
|
+
user.admin?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update?
|
|
20
|
+
user.admin?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destroy?
|
|
24
|
+
user.admin? && record != user # Can't delete yourself
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DataMigrationUserPolicy < ApplicationPolicy
|
|
4
|
+
def index?
|
|
5
|
+
user.can_manage_users?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show?
|
|
9
|
+
user.can_manage_users? || record.id == user.id # Users can view their own profile
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create?
|
|
13
|
+
user.can_manage_users?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update?
|
|
17
|
+
user.can_manage_users? || record.id == user.id # Users can update their own profile
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy?
|
|
21
|
+
user.can_manage_users? && record.id != user.id # Can't delete yourself
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def change_role?
|
|
25
|
+
user.can_manage_users? && record.id != user.id # Can't change your own role
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Scope < Scope
|
|
29
|
+
def resolve
|
|
30
|
+
if user.can_manage_users?
|
|
31
|
+
scope.all # Admins see all users
|
|
32
|
+
else
|
|
33
|
+
scope.where(id: user.id) # Others only see themselves
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MigrationExecutionPolicy < ApplicationPolicy
|
|
4
|
+
def index?
|
|
5
|
+
true # All authenticated users can view execution history
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show?
|
|
9
|
+
true # All authenticated users can view execution details
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create?
|
|
13
|
+
user.can_execute_migrations?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def download?
|
|
17
|
+
true # All authenticated users can download exports
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cancel?
|
|
21
|
+
user.can_execute_migrations? && record.running?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Scope < Scope
|
|
25
|
+
def resolve
|
|
26
|
+
if user.viewer?
|
|
27
|
+
scope.where(status: %i[completed failed]) # Viewers only see finished executions
|
|
28
|
+
else
|
|
29
|
+
scope.all # Operators and admins see everything including running
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MigrationPlanPolicy < ApplicationPolicy
|
|
4
|
+
def index?
|
|
5
|
+
true # All authenticated users can view plans
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show?
|
|
9
|
+
true # All authenticated users can view a plan
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create?
|
|
13
|
+
user.admin?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update?
|
|
17
|
+
user.admin?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy?
|
|
21
|
+
user.admin? && record.can_be_deleted?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute?
|
|
25
|
+
user.can_execute_migrations?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def export_config?
|
|
29
|
+
true # All authenticated users can export plan configuration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def import_config?
|
|
33
|
+
user.admin? # Only admins can import plan configurations
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Scope < Scope
|
|
37
|
+
def resolve
|
|
38
|
+
scope.all # All users can see all plans
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MigrationStepPolicy < ApplicationPolicy
|
|
4
|
+
def index?
|
|
5
|
+
true # All authenticated users can view steps
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create?
|
|
13
|
+
user.admin?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update?
|
|
17
|
+
user.admin?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy?
|
|
21
|
+
user.admin?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Scope < Scope
|
|
25
|
+
def resolve
|
|
26
|
+
scope.all
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataMigration
|
|
4
|
+
class ModelRegistry
|
|
5
|
+
CACHE_KEY = 'data_migration_model_registry'
|
|
6
|
+
CACHE_EXPIRY = 1.hour
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def all_models
|
|
10
|
+
Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_EXPIRY) do
|
|
11
|
+
load_models
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def model_metadata(model_name)
|
|
16
|
+
all_models[model_name]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def model_names
|
|
20
|
+
all_models.keys.sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def refresh!
|
|
24
|
+
Rails.cache.delete(CACHE_KEY)
|
|
25
|
+
all_models
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def load_models
|
|
31
|
+
models_data = {}
|
|
32
|
+
|
|
33
|
+
# Ensure all models are loaded
|
|
34
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
|
35
|
+
|
|
36
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
37
|
+
# Skip engine models, abstract classes, and internal Rails models
|
|
38
|
+
next if model.name.nil?
|
|
39
|
+
next if model.name.start_with?('DataMigration', 'ActiveStorage', 'ApplicationRecord')
|
|
40
|
+
next if model.abstract_class?
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
models_data[model.name] = extract_model_metadata(model)
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Rails.logger.warn "Failed to extract metadata for #{model.name}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
models_data
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_model_metadata(model)
|
|
53
|
+
metadata = {
|
|
54
|
+
name: model.name,
|
|
55
|
+
table_name: model.table_name,
|
|
56
|
+
columns: [],
|
|
57
|
+
attachments: [],
|
|
58
|
+
associations: {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Extract column information
|
|
62
|
+
model.columns.each do |column|
|
|
63
|
+
metadata[:columns] << {
|
|
64
|
+
name: column.name,
|
|
65
|
+
type: column.type.to_s,
|
|
66
|
+
sql_type: column.sql_type
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extract Active Storage attachments
|
|
71
|
+
if model.respond_to?(:reflect_on_all_attachments)
|
|
72
|
+
model.reflect_on_all_attachments.each do |attachment|
|
|
73
|
+
metadata[:attachments] << {
|
|
74
|
+
name: attachment.name.to_s,
|
|
75
|
+
type: attachment.macro.to_s # :has_one_attached or :has_many_attached
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Extract associations
|
|
81
|
+
model.reflect_on_all_associations.each do |association|
|
|
82
|
+
next if association.class_name.start_with?('ActiveStorage', 'DataMigration')
|
|
83
|
+
|
|
84
|
+
metadata[:associations][association.name.to_s] = {
|
|
85
|
+
type: association.macro.to_s,
|
|
86
|
+
class_name: association.class_name,
|
|
87
|
+
foreign_key: association.foreign_key
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
metadata
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|