motor-admin-cstham8 0.4.35

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 (167) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +230 -0
  4. data/Rakefile +11 -0
  5. data/app/channels/motor/application_cable/channel.rb +14 -0
  6. data/app/channels/motor/application_cable/connection.rb +27 -0
  7. data/app/channels/motor/notes_channel.rb +9 -0
  8. data/app/channels/motor/notifications_channel.rb +9 -0
  9. data/app/controllers/concerns/motor/current_ability.rb +21 -0
  10. data/app/controllers/concerns/motor/current_user_method.rb +18 -0
  11. data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
  12. data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
  13. data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
  14. data/app/controllers/motor/alerts_controller.rb +82 -0
  15. data/app/controllers/motor/api_base_controller.rb +33 -0
  16. data/app/controllers/motor/api_configs_controller.rb +54 -0
  17. data/app/controllers/motor/application_controller.rb +8 -0
  18. data/app/controllers/motor/assets_controller.rb +43 -0
  19. data/app/controllers/motor/audits_controller.rb +16 -0
  20. data/app/controllers/motor/auth_tokens_controller.rb +36 -0
  21. data/app/controllers/motor/configs_controller.rb +33 -0
  22. data/app/controllers/motor/dashboards_controller.rb +64 -0
  23. data/app/controllers/motor/data_controller.rb +88 -0
  24. data/app/controllers/motor/forms_controller.rb +61 -0
  25. data/app/controllers/motor/icons_controller.rb +22 -0
  26. data/app/controllers/motor/note_tags_controller.rb +13 -0
  27. data/app/controllers/motor/notes_controller.rb +58 -0
  28. data/app/controllers/motor/notifications_controller.rb +33 -0
  29. data/app/controllers/motor/queries_controller.rb +64 -0
  30. data/app/controllers/motor/reminders_controller.rb +38 -0
  31. data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
  32. data/app/controllers/motor/resource_methods_controller.rb +23 -0
  33. data/app/controllers/motor/resources_controller.rb +26 -0
  34. data/app/controllers/motor/run_api_requests_controller.rb +56 -0
  35. data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
  36. data/app/controllers/motor/run_queries_controller.rb +77 -0
  37. data/app/controllers/motor/schema_controller.rb +31 -0
  38. data/app/controllers/motor/send_alerts_controller.rb +26 -0
  39. data/app/controllers/motor/sessions_controller.rb +23 -0
  40. data/app/controllers/motor/slack_conversations_controller.rb +11 -0
  41. data/app/controllers/motor/tags_controller.rb +11 -0
  42. data/app/controllers/motor/ui_controller.rb +51 -0
  43. data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
  44. data/app/jobs/motor/alert_sending_job.rb +13 -0
  45. data/app/jobs/motor/application_job.rb +6 -0
  46. data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
  47. data/app/jobs/motor/notify_reminder_job.rb +9 -0
  48. data/app/mailers/motor/alerts_mailer.rb +39 -0
  49. data/app/mailers/motor/application_mailer.rb +33 -0
  50. data/app/mailers/motor/notifications_mailer.rb +33 -0
  51. data/app/models/motor/alert.rb +30 -0
  52. data/app/models/motor/alert_lock.rb +7 -0
  53. data/app/models/motor/api_config.rb +28 -0
  54. data/app/models/motor/application_record.rb +18 -0
  55. data/app/models/motor/audit.rb +13 -0
  56. data/app/models/motor/config.rb +13 -0
  57. data/app/models/motor/dashboard.rb +26 -0
  58. data/app/models/motor/form.rb +23 -0
  59. data/app/models/motor/note.rb +18 -0
  60. data/app/models/motor/note_tag.rb +7 -0
  61. data/app/models/motor/note_tag_tag.rb +8 -0
  62. data/app/models/motor/notification.rb +14 -0
  63. data/app/models/motor/query.rb +33 -0
  64. data/app/models/motor/reminder.rb +13 -0
  65. data/app/models/motor/resource.rb +15 -0
  66. data/app/models/motor/tag.rb +7 -0
  67. data/app/models/motor/taggable_tag.rb +8 -0
  68. data/app/views/layouts/motor/application.html.erb +17 -0
  69. data/app/views/layouts/motor/mailer.html.erb +72 -0
  70. data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
  71. data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
  72. data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
  73. data/app/views/motor/ui/show.html.erb +1 -0
  74. data/config/locales/el.yml +420 -0
  75. data/config/locales/en.yml +340 -0
  76. data/config/locales/es.yml +420 -0
  77. data/config/locales/ja.yml +340 -0
  78. data/config/locales/pt.yml +416 -0
  79. data/config/routes.rb +65 -0
  80. data/lib/generators/motor/install_generator.rb +24 -0
  81. data/lib/generators/motor/install_notes_generator.rb +22 -0
  82. data/lib/generators/motor/migration.rb +17 -0
  83. data/lib/generators/motor/templates/install.rb +271 -0
  84. data/lib/generators/motor/templates/install_api_configs.rb +86 -0
  85. data/lib/generators/motor/templates/install_notes.rb +83 -0
  86. data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
  87. data/lib/generators/motor/upgrade_generator.rb +43 -0
  88. data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
  89. data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
  90. data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
  91. data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
  92. data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
  93. data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
  94. data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
  95. data/lib/motor/active_record_utils/types.rb +64 -0
  96. data/lib/motor/active_record_utils.rb +45 -0
  97. data/lib/motor/admin.rb +141 -0
  98. data/lib/motor/alerts/persistance.rb +97 -0
  99. data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
  100. data/lib/motor/alerts/scheduler.rb +30 -0
  101. data/lib/motor/alerts/slack_sender.rb +74 -0
  102. data/lib/motor/alerts.rb +52 -0
  103. data/lib/motor/api_configs.rb +41 -0
  104. data/lib/motor/api_query/apply_scope.rb +44 -0
  105. data/lib/motor/api_query/build_json.rb +171 -0
  106. data/lib/motor/api_query/build_meta.rb +20 -0
  107. data/lib/motor/api_query/filter.rb +125 -0
  108. data/lib/motor/api_query/paginate.rb +19 -0
  109. data/lib/motor/api_query/search.rb +60 -0
  110. data/lib/motor/api_query/sort.rb +64 -0
  111. data/lib/motor/api_query.rb +24 -0
  112. data/lib/motor/assets.rb +62 -0
  113. data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
  114. data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
  115. data/lib/motor/build_schema/apply_permissions.rb +64 -0
  116. data/lib/motor/build_schema/defaults.rb +66 -0
  117. data/lib/motor/build_schema/find_display_column.rb +65 -0
  118. data/lib/motor/build_schema/find_icon.rb +135 -0
  119. data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
  120. data/lib/motor/build_schema/load_from_rails.rb +361 -0
  121. data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
  122. data/lib/motor/build_schema/reorder_schema.rb +88 -0
  123. data/lib/motor/build_schema/utils.rb +31 -0
  124. data/lib/motor/build_schema.rb +125 -0
  125. data/lib/motor/cancan_utils/ability_patch.rb +31 -0
  126. data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
  127. data/lib/motor/cancan_utils.rb +9 -0
  128. data/lib/motor/configs/build_configs_hash.rb +90 -0
  129. data/lib/motor/configs/build_ui_app_tag.rb +177 -0
  130. data/lib/motor/configs/load_from_cache.rb +110 -0
  131. data/lib/motor/configs/sync_from_file.rb +35 -0
  132. data/lib/motor/configs/sync_from_hash.rb +159 -0
  133. data/lib/motor/configs/sync_middleware.rb +72 -0
  134. data/lib/motor/configs/sync_with_remote.rb +47 -0
  135. data/lib/motor/configs/write_to_file.rb +36 -0
  136. data/lib/motor/configs.rb +39 -0
  137. data/lib/motor/dashboards/persistance.rb +73 -0
  138. data/lib/motor/dashboards.rb +8 -0
  139. data/lib/motor/forms/persistance.rb +93 -0
  140. data/lib/motor/forms.rb +8 -0
  141. data/lib/motor/hash_serializer.rb +21 -0
  142. data/lib/motor/net_http_utils.rb +50 -0
  143. data/lib/motor/notes/notify_mentions.rb +71 -0
  144. data/lib/motor/notes/notify_reminder.rb +48 -0
  145. data/lib/motor/notes/persist.rb +36 -0
  146. data/lib/motor/notes/reminders_scheduler.rb +39 -0
  147. data/lib/motor/notes/tags.rb +34 -0
  148. data/lib/motor/notes.rb +12 -0
  149. data/lib/motor/queries/persistance.rb +90 -0
  150. data/lib/motor/queries/postgresql_exec_query.rb +28 -0
  151. data/lib/motor/queries/render_sql_template.rb +61 -0
  152. data/lib/motor/queries/run_query.rb +289 -0
  153. data/lib/motor/queries.rb +11 -0
  154. data/lib/motor/railtie.rb +11 -0
  155. data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
  156. data/lib/motor/resources/fetch_configured_model.rb +269 -0
  157. data/lib/motor/resources/persist_configs.rb +232 -0
  158. data/lib/motor/resources.rb +19 -0
  159. data/lib/motor/slack/client.rb +62 -0
  160. data/lib/motor/slack.rb +16 -0
  161. data/lib/motor/tags.rb +32 -0
  162. data/lib/motor/tasks/motor.rake +54 -0
  163. data/lib/motor/version.rb +5 -0
  164. data/lib/motor-admin-cstham8.rb +3 -0
  165. data/lib/motor.rb +87 -0
  166. data/ui/dist/manifest.json +1990 -0
  167. metadata +303 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module FindDisplayColumn
6
+ DISPLAY_NAMES = Set.new(
7
+ %w[
8
+ name
9
+ full_name
10
+ fullname
11
+ last_name
12
+ lastname
13
+ first_name
14
+ firstname
15
+ fname
16
+ lname
17
+ sname
18
+ company
19
+ domain
20
+ website
21
+ title
22
+ phone
23
+ phone_number
24
+ email
25
+ phone
26
+ filename
27
+ file_name
28
+ url
29
+ make
30
+ brand
31
+ manufacturer
32
+ model
33
+ address
34
+ code
35
+ ]
36
+ ).freeze
37
+
38
+ DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES.to_a).source, Regexp::IGNORECASE)
39
+
40
+ module_function
41
+
42
+ def call(model)
43
+ column_names = fetch_column_names(model)
44
+
45
+ select_column_name(column_names)
46
+ end
47
+
48
+ def select_column_name(column_names)
49
+ name = column_names.find { |column_name| column_name.in?(DISPLAY_NAMES) }
50
+ name ||= column_names.find { |column_name| column_name.match?(DISPLAY_NAME_REGEXP) }
51
+
52
+ name
53
+ end
54
+
55
+ def fetch_column_names(model)
56
+ model.columns.map do |column|
57
+ next unless column.type.in?(BuildSchema::SEARCHABLE_COLUMN_TYPES)
58
+ next if column.try(:array?)
59
+
60
+ column.name
61
+ end.compact
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module FindIcon
5
+ ICONS_MAP = {
6
+ 'audit' => 'history',
7
+ 'block' => 'ban',
8
+ 'blocked' => 'ban',
9
+ 'blacklisted' => 'ban',
10
+ 'blocklisted' => 'ban',
11
+ 'banned' => 'ban',
12
+ 'website' => 'world',
13
+ 'location' => 'gps',
14
+ 'photo' => 'photo',
15
+ 'image' => 'photo',
16
+ 'screenshot' => 'photo',
17
+ 'picture' => 'photo',
18
+ 'video' => 'video',
19
+ 'file' => 'file',
20
+ 'pdf' => 'file-text',
21
+ 'ad' => 'ad',
22
+ 'shop' => 'building-store',
23
+ 'store' => 'building-store',
24
+ 'seller' => 'building-store',
25
+ 'app' => 'apps',
26
+ 'book' => 'book',
27
+ 'movie' => 'movie',
28
+ 'country' => 'map',
29
+ 'api' => 'tool',
30
+ 'config' => 'tool',
31
+ 'key' => 'key',
32
+ 'phone' => 'phone',
33
+ 'call' => 'phone',
34
+ 'cell' => 'phone',
35
+ 'token' => 'key',
36
+ 'secret' => 'lock',
37
+ 'automation' => 'manual-gearbox',
38
+ 'workflow' => 'manual-gearbox',
39
+ 'relationship' => 'hierarchy',
40
+ 'person' => 'user',
41
+ 'people' => 'users',
42
+ 'mail' => 'mail',
43
+ 'user' => 'user',
44
+ 'rating' => 'star',
45
+ 'article' => 'blockquote',
46
+ 'podcast' => 'headphones',
47
+ 'payment' => 'cash',
48
+ 'log' => 'history',
49
+ 'history' => 'history',
50
+ 'rule' => 'manual-gearbox',
51
+ 'tracking' => 'zoom-question',
52
+ 'github' => 'brand-github',
53
+ 'tag' => 'hash',
54
+ 'category' => 'hash',
55
+ 'label' => 'hash',
56
+ 'twitter' => 'brand-twitter',
57
+ 'facebook' => 'brand-facebook',
58
+ 'google' => 'brand-google',
59
+ 'renewal' => 'repeat',
60
+ 'sales' => 'report-money',
61
+ 'telegram' => 'brand-telegram',
62
+ 'whatsapp' => 'brand-whatsapp',
63
+ 'survey' => 'checkbox',
64
+ 'subcategory' => 'hash',
65
+ 'type' => 'hash',
66
+ 'model' => 'hash',
67
+ 'taxon' => 'hash',
68
+ 'affiliate' => 'affiliate',
69
+ 'chat' => 'message-circle',
70
+ 'message' => 'messages',
71
+ 'poll' => 'messages',
72
+ 'feedpack' => 'messages',
73
+ 'attachment' => 'paperclip',
74
+ 'certificate' => 'certificate',
75
+ 'approval' => 'certificate',
76
+ 'bank' => 'building-bank',
77
+ 'bridge' => 'building-bridge',
78
+ 'car' => 'car',
79
+ 'vehicle' => 'car',
80
+ 'analytic' => 'device-analytics',
81
+ 'stat' => 'device-analytics',
82
+ 'statistic' => 'device-analytics',
83
+ 'link' => 'link',
84
+ 'slug' => 'link',
85
+ 'city' => 'building-skyscraper',
86
+ 'region' => 'map',
87
+ 'district' => 'building-community',
88
+ 'community' => 'building-community',
89
+ 'activity' => 'activity',
90
+ 'invoice' => 'file-invoice',
91
+ 'settlement' => 'file-invoice',
92
+ 'state' => 'map',
93
+ 'note' => 'note',
94
+ 'order' => 'truck-delivery',
95
+ 'lead' => 'building-lighthouse',
96
+ 'error' => 'file-alert',
97
+ 'page' => 'brand-pagekit',
98
+ 'date' => 'calendar-event',
99
+ 'customer' => 'users',
100
+ 'client' => 'users',
101
+ 'ticket' => 'ticket',
102
+ 'contact' => 'users',
103
+ 'member' => 'users',
104
+ 'admin' => 'user-check',
105
+ 'notification' => 'bell',
106
+ 'alert' => 'bell',
107
+ 'team' => 'users',
108
+ 'folder' => 'folder',
109
+ 'subscriber' => 'user-plus',
110
+ 'product' => 'building-store',
111
+ 'html' => 'code',
112
+ 'stripe' => 'brand-stripe',
113
+ 'email' => 'mail',
114
+ 'status' => 'hash'
115
+ }.freeze
116
+
117
+ DEFAULT_ICON = BuildSchema::DEFAULT_ICON
118
+
119
+ module_function
120
+
121
+ def call(text)
122
+ text = text.underscore
123
+ icon = ICONS_MAP[text] || ICONS_MAP[text.singularize]
124
+
125
+ icon ||=
126
+ text.split(%r{[\s_/]}).reverse.reduce(nil) do |_acc, word|
127
+ found_icon = ICONS_MAP[word] || ICONS_MAP[word.singularize]
128
+
129
+ break found_icon if found_icon
130
+ end
131
+
132
+ icon || DEFAULT_ICON
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module FindSearchableColumns
6
+ SELECT_COLUMNS_AMOUNT = 2
7
+ COLUMN_TYPES = BuildSchema::SEARCHABLE_COLUMN_TYPES
8
+
9
+ module_function
10
+
11
+ def call(model)
12
+ columns = find_searchable_columns(model)
13
+
14
+ selected_columns =
15
+ columns.grep(BuildSchema::FindDisplayColumn::DISPLAY_NAME_REGEXP).presence
16
+
17
+ selected_columns ||= columns.first(SELECT_COLUMNS_AMOUNT)
18
+
19
+ ([model.primary_key] + selected_columns).compact.sort
20
+ end
21
+
22
+ def find_searchable_columns(model)
23
+ model.columns.map do |column|
24
+ next unless column.type.in?(COLUMN_TYPES)
25
+ next if column.try(:array?)
26
+ next if model.validators_on(column.name).any?(ActiveModel::Validations::InclusionValidator)
27
+
28
+ column.name
29
+ end.compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module LoadFromRails
6
+ MUTEX = Mutex.new
7
+ UNIFIED_TYPES = ActiveRecordUtils::Types::UNIFIED_TYPES
8
+
9
+ I18N_SCOPES_KEY = 'activerecord.scopes'
10
+
11
+ ACTION_TEXT_REFLECTION_PREFIX = 'rich_text_'
12
+ ACTION_TEXT_COLUMN_SUFFIX = '_body'
13
+ ACTION_TEXT_SCOPE_PREFIX = 'with_rich_text_'
14
+ ACTIVE_STORAGE_SCOPE_PREFIX = 'with_attached_'
15
+
16
+ DEFAULT_CURRENCY_FORMAT_HASH = {
17
+ currency: 'USD',
18
+ currency_base: 'units'
19
+ }.freeze
20
+
21
+ module_function
22
+
23
+ def call
24
+ models.map do |model|
25
+ model = Object.const_get(model.name)
26
+
27
+ next unless model.table_exists?
28
+
29
+ schema = build_model_schema(model)
30
+
31
+ if model.respond_to?(:devise_modules)
32
+ Motor::BuildSchema::AdjustDeviseModelSchema.call(schema, model.devise_modules)
33
+ end
34
+
35
+ schema
36
+ rescue StandardError, NotImplementedError => e
37
+ Rails.logger.error(e)
38
+
39
+ next
40
+ end.compact.uniq
41
+ end
42
+
43
+ def models
44
+ eager_load_models!
45
+
46
+ models = ActiveRecord::Base.descendants.reject { |k| k.abstract_class || k.anonymous? }
47
+
48
+ models -= Motor::ApplicationRecord.descendants
49
+ models -= [Motor::Audit]
50
+ models -= [ActiveRecord::SchemaMigration] if defined?(ActiveRecord::SchemaMigration)
51
+ models -= [ActiveRecord::InternalMetadata] if defined?(ActiveRecord::InternalMetadata)
52
+ models -= [ActiveStorage::Blob] if defined?(ActiveStorage::Blob)
53
+ models -= [ActionText::RichText] if defined?(ActionText::RichText)
54
+ models -= [ActiveStorage::VariantRecord] if defined?(ActiveStorage::VariantRecord)
55
+
56
+ models
57
+ end
58
+
59
+ # rubocop:disable Metrics/MethodLength
60
+ def build_model_schema(model)
61
+ model_name = model.name
62
+
63
+ return Motor::BuildSchema::ActiveStorageAttachmentSchema.call if model_name == 'ActiveStorage::Attachment'
64
+
65
+ {
66
+ name: model_name.underscore,
67
+ slug: Utils.slugify(model),
68
+ table_name: model.table_name,
69
+ class_name: model_name,
70
+ primary_key: model.primary_key,
71
+ display_name: model.model_name.human(count: :many, default: model_name.titleize.pluralize),
72
+ display_column: FindDisplayColumn.call(model),
73
+ columns: fetch_columns(model),
74
+ associations: fetch_associations(model),
75
+ icon: Motor::FindIcon.call(model_name),
76
+ scopes: fetch_scopes(model),
77
+ actions: BuildSchema::Defaults.actions,
78
+ tabs: BuildSchema::Defaults.tabs,
79
+ searchable_columns: FindSearchableColumns.call(model),
80
+ custom_sql: nil,
81
+ visible: true,
82
+ display_primary_key: true,
83
+ preferences: {
84
+ display_as: 'table'
85
+ }
86
+ }.with_indifferent_access
87
+ end
88
+ # rubocop:enable Metrics/MethodLength
89
+
90
+ def fetch_scopes(model)
91
+ model.defined_scopes.map do |scope_name|
92
+ scope_name = scope_name.to_s
93
+
94
+ next if scope_name.starts_with?(ACTIVE_STORAGE_SCOPE_PREFIX)
95
+ next if scope_name.starts_with?(ACTION_TEXT_SCOPE_PREFIX)
96
+
97
+ {
98
+ name: scope_name,
99
+ display_name: I18n.t(scope_name,
100
+ scope: [I18N_SCOPES_KEY, model.name.underscore].join('.'),
101
+ default: scope_name.humanize),
102
+ scope_type: DEFAULT_TYPE,
103
+ visible: true,
104
+ preferences: {}
105
+ }
106
+ end.compact
107
+ end
108
+
109
+ def fetch_columns(model)
110
+ default_attrs = model.new.attributes
111
+
112
+ reference_columns = fetch_reference_columns(model)
113
+
114
+ table_columns =
115
+ model.columns.map do |column|
116
+ next if reference_columns.find { |c| c[:name] == column.name }
117
+
118
+ build_table_column(column, model, default_attrs)
119
+ end.compact
120
+
121
+ reference_columns + table_columns
122
+ end
123
+
124
+ def build_table_column(column, model, default_attrs)
125
+ access_type =
126
+ if model.primary_key == column.name
127
+ ColumnAccessTypes::READ_ONLY
128
+ else
129
+ COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE)
130
+ end
131
+
132
+ {
133
+ name: column.name,
134
+ display_name: Utils.humanize_column_name(model.human_attribute_name(column.name)),
135
+ column_type: fetch_column_type(column, model),
136
+ column_source: ColumnSources::TABLE,
137
+ is_array: column.try(:array?),
138
+ access_type: access_type,
139
+ default_value: default_attrs[column.name],
140
+ validators: fetch_validators(model, column.name),
141
+ reference: nil,
142
+ format: fetch_format_hash(column, model),
143
+ description: '',
144
+ virtual: false
145
+ }
146
+ end
147
+
148
+ def fetch_format_hash(column, model)
149
+ return DEFAULT_CURRENCY_FORMAT_HASH if column.name == 'price'
150
+
151
+ inclusion_validator, = model.validators_on(column.name).grep(ActiveModel::Validations::InclusionValidator)
152
+
153
+ return { select_options: inclusion_validator.send(:delimiter) } if inclusion_validator
154
+
155
+ enum = model.defined_enums[column.name]
156
+
157
+ return { select_options: enum.keys } if enum
158
+
159
+ return {} if column.name == 'year'
160
+
161
+ return { number_format: true } if !column.name == 'id' &&
162
+ !column.name.match?(/_(?:id|year)\z/) &&
163
+ %i[integer float].include?(column.type)
164
+
165
+ {}
166
+ end
167
+
168
+ def fetch_column_type(column, model)
169
+ return ColumnTypes::CURRENCY if column.name == 'price'
170
+ return ColumnTypes::COLOR if %w[hex color].include?(column.name)
171
+ return ColumnTypes::TAG if model.defined_enums[column.name]
172
+ return ColumnTypes::TAG if model.validators_on(column.name).any?(ActiveModel::Validations::InclusionValidator)
173
+ return ColumnTypes::RICHTEXT if column.name.ends_with?('_html')
174
+ return ColumnTypes::COLOR if column.name.match?(/_(color|hex)\z/)
175
+
176
+ UNIFIED_TYPES[column.type.to_s] || column.type.to_s
177
+ end
178
+
179
+ def fetch_reference_columns(model)
180
+ default_attrs = model.new.attributes
181
+
182
+ model.reflections.map do |name, ref|
183
+ next if !ref.has_one? && !ref.belongs_to?
184
+
185
+ unless ref.polymorphic?
186
+ begin
187
+ next if ref.klass.name == 'ActiveStorage::Blob'
188
+ rescue StandardError => e
189
+ Rails.logger.error(e)
190
+
191
+ next
192
+ end
193
+ end
194
+
195
+ build_reflection_column(name, model, ref, default_attrs)
196
+ end.compact
197
+ end
198
+
199
+ # rubocop:disable Metrics/AbcSize
200
+ def build_reflection_column(name, model, ref, default_attrs)
201
+ if !ref.polymorphic? && ref.klass.name == 'ActionText::RichText'
202
+ return build_action_text_column(name, model, ref)
203
+ end
204
+
205
+ column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
206
+ is_attachment = !ref.polymorphic? && ref.klass.name == 'ActiveStorage::Attachment'
207
+ access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
208
+ column_type = is_attachment ? ColumnTypes::FILE : ColumnTypes::REFERENCE
209
+ column_source = model.columns_hash[column_name] ? ColumnSources::TABLE : ColumnSources::REFLECTION
210
+
211
+ {
212
+ name: column_name,
213
+ display_name: model.human_attribute_name(name),
214
+ column_type: column_type,
215
+ column_source: column_source,
216
+ access_type: access_type,
217
+ default_value: default_attrs[column_name],
218
+ validators: fetch_validators(model, column_name, ref),
219
+ format: {},
220
+ reference: build_reference(model, name, ref),
221
+ virtual: false
222
+ }
223
+ end
224
+ # rubocop:enable Metrics/AbcSize
225
+
226
+ def build_action_text_column(name, model, ref)
227
+ name = name.delete_prefix(ACTION_TEXT_REFLECTION_PREFIX)
228
+
229
+ {
230
+ name: name + ACTION_TEXT_COLUMN_SUFFIX,
231
+ display_name: model.human_attribute_name(name),
232
+ column_type: ColumnTypes::RICHTEXT,
233
+ column_source: ColumnSources::REFLECTION,
234
+ access_type: ColumnAccessTypes::READ_WRITE,
235
+ default_value: '',
236
+ validators: fetch_validators(model, name, ref),
237
+ format: {},
238
+ reference: nil,
239
+ virtual: true
240
+ }
241
+ end
242
+
243
+ def build_reference(model, name, reflection)
244
+ primary_key = reflection.polymorphic? ? 'id' : reflection.join_primary_key
245
+ foreign_key = reflection.join_foreign_key
246
+
247
+ primary_key, foreign_key = foreign_key, primary_key if reflection.has_one?
248
+
249
+ {
250
+ name: name,
251
+ display_name: model.human_attribute_name(name),
252
+ model_name: reflection.polymorphic? ? nil : reflection.klass.name.underscore,
253
+ reference_type: reflection.belongs_to? ? 'belongs_to' : 'has_one',
254
+ foreign_key: foreign_key,
255
+ primary_key: primary_key,
256
+ options: reflection.options.slice(:through, :source),
257
+ polymorphic: reflection.polymorphic?,
258
+ virtual: false
259
+ }
260
+ end
261
+
262
+ def fetch_associations(model)
263
+ model.reflections.map do |name, ref|
264
+ next if ref.has_one? || ref.belongs_to?
265
+ next unless valid_reflection?(ref)
266
+
267
+ model_class = ref.klass
268
+
269
+ next if model_class.name == 'ActiveStorage::Blob'
270
+
271
+ {
272
+ name: name,
273
+ display_name: model.human_attribute_name(name),
274
+ slug: name.underscore,
275
+ model_name: model_class.name.underscore,
276
+ foreign_key: ref.join_primary_key,
277
+ primary_key: ref.join_foreign_key,
278
+ polymorphic: ref.options[:as].present?,
279
+ icon: Motor::FindIcon.call(name),
280
+ options: ref.options.slice(:through, :source),
281
+ virtual: false,
282
+ visible: true
283
+ }
284
+ end.compact
285
+ end
286
+
287
+ def fetch_validators(model, column_name, reflection = nil)
288
+ validators =
289
+ if reflection&.belongs_to? && !reflection.options[:optional]
290
+ [{ required: true }]
291
+ else
292
+ []
293
+ end
294
+
295
+ enum = model.defined_enums[column_name]
296
+
297
+ validators += [{ includes: enum.keys }] if enum
298
+
299
+ validators += model.validators_on(column_name).map do |validator|
300
+ next if validator.options[:if] || validator.options[:unless]
301
+
302
+ build_validator_hash(validator)
303
+ end.compact
304
+
305
+ validators.uniq
306
+ end
307
+
308
+ def build_validator_hash(validator)
309
+ options = validator.options.reject { |_, v| v.is_a?(Proc) || v.is_a?(Symbol) }
310
+
311
+ case validator
312
+ when ActiveModel::Validations::InclusionValidator
313
+ { includes: validator.send(:delimiter) }
314
+ when ActiveRecord::Validations::PresenceValidator
315
+ { required: true }
316
+ when ActiveRecord::Validations::LengthValidator
317
+ { length: normalize_length_validation_options(options) }
318
+ when ActiveModel::Validations::NumericalityValidator
319
+ { numeric: options }
320
+ end
321
+ end
322
+
323
+ def normalize_length_validation_options(options)
324
+ return options if options[:in].blank?
325
+
326
+ in_range = options[:in]
327
+
328
+ options.merge(in: in_range.minmax)
329
+ end
330
+
331
+ def valid_reflection?(reflection)
332
+ reflection.klass
333
+ reflection.foreign_key
334
+
335
+ true
336
+ rescue StandardError => e
337
+ Rails.logger.error(e)
338
+
339
+ false
340
+ end
341
+
342
+ def eager_load_models!
343
+ MUTEX.synchronize do
344
+ if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
345
+ Zeitwerk::Loader.eager_load_all
346
+ else
347
+ Rails.application.eager_load!
348
+ end
349
+
350
+ ActiveRecord::Base.descendants.each do |model|
351
+ model.reflections.each_value do |ref|
352
+ ref.klass
353
+ rescue StandardError
354
+ next
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end