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,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Queries
5
+ module RunQuery
6
+ DEFAULT_LIMIT = 100_000
7
+
8
+ QueryResult = Struct.new(:data, :columns, :error, keyword_init: true)
9
+
10
+ SUBQUERY_NAME = '__query__'
11
+
12
+ STATEMENT_VARIABLE_REGEXP = /\$\d+/.freeze
13
+ PG_ERROR_REGEXP = /\APG.+ERROR:/.freeze
14
+
15
+ RESERVED_VARIABLES = %w[current_user_id current_user_email].freeze
16
+
17
+ DATABASE_URL_VARIABLE_SUFFIX = '_database_url'
18
+ QUERY_VARIABLE_PREFIX = 'query_'
19
+
20
+ DB_LINK_VALIDATE_REGEXP = /(.*?)\s*\{\{\s*\w+_database_url\s*\}\}/i.freeze
21
+
22
+ UnknownDatabase = Class.new(StandardError)
23
+ UnsafeDatabaseUrlUsage = Class.new(StandardError)
24
+
25
+ module_function
26
+
27
+ # @param query [Motor::Query]
28
+ # @param variables_hash [Hash]
29
+ # @param limit [Integer]
30
+ # @param filters [Hash]
31
+ # @return [Motor::Queries::RunQuery::QueryResult]
32
+ def call!(query, variables_hash: nil, limit: nil, filters: nil)
33
+ variables_hash ||= {}
34
+ variables_hash = variables_hash.with_indifferent_access
35
+ limit ||= DEFAULT_LIMIT
36
+ filters ||= {}
37
+
38
+ result = execute_query(query, limit, variables_hash, filters)
39
+
40
+ QueryResult.new(data: result.rows, columns: build_columns_hash(result))
41
+ end
42
+
43
+ # @param query [Motor::Query]
44
+ # @param variables_hash [Hash]
45
+ # @param limit [Integer]
46
+ # @return [Motor::Queries::RunQuery::QueryResult]
47
+ def call(query, variables_hash: nil, limit: nil, filters: nil)
48
+ call!(query, variables_hash: variables_hash, limit: limit, filters: filters)
49
+ rescue ActiveRecord::StatementInvalid => e
50
+ QueryResult.new(error: build_error_message(e))
51
+ end
52
+
53
+ # @param exception [ActiveRecord::StatementInvalid]
54
+ # @return [String]
55
+ def build_error_message(exception)
56
+ exception.message.sub(PG_ERROR_REGEXP, '').strip.upcase_first
57
+ end
58
+
59
+ # @param query [Motor::Query]
60
+ # @param limit [Integer]
61
+ # @param variables_hash [Hash]
62
+ # @param filters [Hash]
63
+ # @return [ActiveRecord::Result]
64
+ def execute_query(query, limit, variables_hash, filters)
65
+ connection_class = fetch_connection_class(query)
66
+
67
+ statement = prepare_sql_statement(connection_class, query, limit, variables_hash, filters)
68
+
69
+ case connection_class.connection.class.name
70
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
71
+ PostgresqlExecQuery.call(connection_class.connection, statement)
72
+ else
73
+ statement = normalize_statement_for_sql(statement)
74
+
75
+ connection_class.connection.exec_query(*statement)
76
+ end
77
+ end
78
+
79
+ def validate_query!(sql)
80
+ return if sql.scan(DB_LINK_VALIDATE_REGEXP).flatten.all? { |line| line.ends_with?('dblink(') }
81
+
82
+ raise UnsafeDatabaseUrlUsage, 'Database URL variable is allowed only with dblink'
83
+ end
84
+
85
+ # @param result [ActiveRecord::Result]
86
+ # @return [Hash]
87
+ def build_columns_hash(result)
88
+ result.columns.map.with_index do |column_name, index|
89
+ column_type_class = result.column_types[column_name]
90
+
91
+ column_type = ActiveRecordUtils::Types.find_name_for_type(column_type_class) if column_type_class
92
+
93
+ column_type ||=
94
+ begin
95
+ not_nil_value = result.rows.reduce(nil) do |acc, row|
96
+ column = row[index]
97
+
98
+ break column unless column.nil?
99
+
100
+ acc
101
+ end
102
+
103
+ fetch_column_type_from_value(not_nil_value)
104
+ end
105
+
106
+ {
107
+ name: column_name,
108
+ display_name: column_name.humanize,
109
+ column_type: column_type,
110
+ is_array: column_type.class.to_s == 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array'
111
+ }
112
+ end
113
+ end
114
+
115
+ # @param value [Object]
116
+ # @return [String]
117
+ def fetch_column_type_from_value(value)
118
+ case value
119
+ when Integer
120
+ 'integer'
121
+ when Float
122
+ 'float'
123
+ when Time
124
+ 'datetime'
125
+ when Date
126
+ 'date'
127
+ when TrueClass, FalseClass
128
+ 'boolean'
129
+ else
130
+ 'string'
131
+ end
132
+ end
133
+
134
+ # @param connection_class [Class]
135
+ # @param query [Motor::Query]
136
+ # @param limit [Integer]
137
+ # @param variables_hash [Hash]
138
+ # @param filters [Hash]
139
+ # @return [Array]
140
+ def prepare_sql_statement(connection_class, query, limit, variables_hash, filters)
141
+ variables = merge_variable_default_values(query.preferences.fetch(:variables, []), variables_hash)
142
+
143
+ validate_query!(query.sql_body)
144
+
145
+ sql, query_variables = RenderSqlTemplate.call(query.sql_body, variables)
146
+ select_sql = build_select_sql(connection_class, sql, limit, filters)
147
+
148
+ attributes = build_statement_attributes(query_variables)
149
+
150
+ [select_sql, 'SQL', attributes]
151
+ end
152
+
153
+ # @param connection_class [Class]
154
+ # @param sql [String]
155
+ # @param limit [Number]
156
+ # @param filters [Hash]
157
+ # @return [String]
158
+ def build_select_sql(connection_class, sql, limit, filters)
159
+ sql = normalize_sql(sql)
160
+
161
+ subquery_sql = Arel.sql("(#{sql})").as(connection_class.connection.quote_column_name(SUBQUERY_NAME))
162
+
163
+ arel_filters = build_filters_arel(filters)
164
+
165
+ rel = connection_class.from(subquery_sql)
166
+ .select(Arel::Table.new(SUBQUERY_NAME)[Arel.star])
167
+ .where(arel_filters)
168
+
169
+ rel = rel.limit(limit.to_i) unless connection_class.connection.class.name.include?('SQLServerAdapter')
170
+
171
+ rel.to_sql
172
+ end
173
+
174
+ # @param filters [Hash]
175
+ # @return [Arel::Nodes, nil]
176
+ def build_filters_arel(filters)
177
+ return nil if filters.blank?
178
+
179
+ table = Arel::Table.new(SUBQUERY_NAME)
180
+
181
+ arel_filters = filters.map { |key, value| table[key].in(value) }
182
+
183
+ arel_filters[1..].reduce(arel_filters.first) { |acc, arel| acc.and(arel) }
184
+ end
185
+
186
+ # @param variables [Array<(String, Object)>]
187
+ # @return [Array<ActiveRecord::Relation::QueryAttribute>]
188
+ def build_statement_attributes(variables)
189
+ variables.map do |variable_name, value|
190
+ [value].flatten.map do |val|
191
+ val = fetch_variable_database_url(variable_name) if variable_name.ends_with?(DATABASE_URL_VARIABLE_SUFFIX)
192
+ val = fetch_query_data(variable_name) if variable_name.starts_with?(QUERY_VARIABLE_PREFIX)
193
+
194
+ ActiveRecord::Relation::QueryAttribute.new(
195
+ variable_name,
196
+ val,
197
+ ActiveRecord::Type::Value.new
198
+ )
199
+ end
200
+ end.flatten
201
+ end
202
+
203
+ def fetch_variable_database_url(variable_name)
204
+ class_name = variable_name.delete_suffix(DATABASE_URL_VARIABLE_SUFFIX).classify
205
+
206
+ Motor::DatabaseClasses.const_get(class_name).connection_db_config.url
207
+ rescue NameError
208
+ raise UnknownDatabase, "#{class_name} database is not defined"
209
+ end
210
+
211
+ def fetch_query_data(variable_name)
212
+ query = Motor::Query.find(variable_name.split('_').last)
213
+
214
+ result = Motor::Queries::RunQuery.call(query)
215
+ columns = result.columns.pluck(:name)
216
+
217
+ result.data.map { |row| columns.zip(row).to_h }.to_json
218
+ end
219
+
220
+ # @param array [Array]
221
+ # @return [Array]
222
+ def normalize_statement_for_sql(statement)
223
+ sql, _, attributes = statement
224
+
225
+ sql = ActiveRecord::Base.send(:replace_bind_variables,
226
+ sql.gsub(STATEMENT_VARIABLE_REGEXP, '?'),
227
+ attributes.map(&:value))
228
+
229
+ [sql, 'SQL', []]
230
+ end
231
+
232
+ def normalize_sql(sql)
233
+ sql.strip.delete_suffix(';').gsub(/\A\)+/, '').gsub(/\z\(+/, '')
234
+ end
235
+
236
+ # @param variable_configs [Array<Hash>]
237
+ # @param variables_hash [Hash]
238
+ # @return [Hash]
239
+ def merge_variable_default_values(variable_configs, variables_hash)
240
+ variable_configs.each_with_object(variables_hash.slice(*RESERVED_VARIABLES)) do |variable, acc|
241
+ next if RESERVED_VARIABLES.include?(variable[:name])
242
+
243
+ acc[variable[:name]] ||= variables_hash[variable[:name]] || variable[:default_value]
244
+ end
245
+ end
246
+
247
+ def fetch_connection_class(query)
248
+ database_name = query.preferences[:database]
249
+
250
+ return default_connection_class if database_name.blank? || database_name == 'default'
251
+
252
+ return default_connection_class if database_name == 'primary'
253
+
254
+ ar_configurations = ActiveRecord::Base.configurations.configurations
255
+ .find { |c| c.name == database_name && c.env_name == Rails.env }
256
+
257
+ if ar_configurations
258
+ fetch_ar_configurations_connection(database_name, ar_configurations)
259
+ else
260
+ Motor::DatabaseClasses.const_get(database_name.sub(/\A\d+/, '').parameterize.underscore.classify)
261
+ end
262
+ end
263
+
264
+ def fetch_ar_configurations_connection(database_name, ar_configurations)
265
+ Motor::DatabaseClasses.const_get(database_name.classify)
266
+ rescue NameError
267
+ klass = Class.new(ActiveRecord::Base)
268
+
269
+ Motor::DatabaseClasses.const_set(database_name.classify, klass)
270
+
271
+ klass.establish_connection(ar_configurations.name.to_sym)
272
+
273
+ klass
274
+ end
275
+
276
+ def find_connection_in_pool(database_name)
277
+ ActiveRecord::Base.connection_pool.connections.find do |conn|
278
+ conn.pool.db_config.name == database_name
279
+ end
280
+ end
281
+
282
+ def default_connection_class
283
+ 'ResourceRecord'.safe_constantize ||
284
+ 'ApplicationRecord'.safe_constantize ||
285
+ Class.new(ActiveRecord::Base).tap { |e| e.abstract_class = true }
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Queries
5
+ end
6
+ end
7
+
8
+ require_relative 'queries/render_sql_template'
9
+ require_relative 'queries/run_query'
10
+ require_relative 'queries/persistance'
11
+ require_relative 'queries/postgresql_exec_query'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :motor_admin
6
+
7
+ rake_tasks do
8
+ Dir[Motor::PATH.join('./motor/tasks/**/*.rake')].each { |f| load f }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Resources
5
+ module CustomSqlColumnsCache
6
+ CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
7
+
8
+ module_function
9
+
10
+ def call(sql)
11
+ CACHE_STORE.fetch(sql.hash) do
12
+ Queries::RunQuery.call(Query.new(sql_body: sql), limit: 0).columns || []
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Resources
5
+ module FetchConfiguredModel
6
+ CACHE_HASH = ActiveSupport::HashWithIndifferentAccess.new
7
+
8
+ HAS_AND_BELONGS_TO_MANY_JOIN_MODEL_PREFIX = 'HABTM_'
9
+
10
+ module_function
11
+
12
+ def call(model, cache_key:)
13
+ configs = Motor::Configs::LoadFromCache.load_resources(cache_key: cache_key)
14
+
15
+ return model if model.name == 'ActiveStorage::Attachment'
16
+ return model if sti_model?(model)
17
+
18
+ maybe_fetch_from_cache(
19
+ model,
20
+ cache_key.to_s + model.object_id.to_s,
21
+ -> { build_configured_model_from_configs(model, configs) },
22
+ ->(klass) { configure_reflection_classes(klass, cache_key) }
23
+ )
24
+ end
25
+
26
+ def build_configured_model(model, config)
27
+ klass = Class.new(model)
28
+ klass.inheritance_column = nil if model.superclass.abstract_class
29
+
30
+ define_class_properties(klass, model)
31
+
32
+ define_columns_hash(klass, config)
33
+ define_default_scope(klass, config)
34
+ define_column_reflections(klass, config)
35
+ define_associations(klass, config)
36
+ define_searchable_columns_method(klass, config)
37
+
38
+ klass
39
+ end
40
+
41
+ def define_default_scope(klass, config)
42
+ return klass if config[:custom_sql].blank?
43
+
44
+ klass.instance_variable_set(:@__motor_custom_sql, config[:custom_sql].squish.delete_suffix(';'))
45
+
46
+ klass.instance_eval do
47
+ default_scope do
48
+ from(Arel.sql("(#{self.klass.instance_variable_get(:@__motor_custom_sql)})")
49
+ .as(connection.quote_column_name(table_name)))
50
+ end
51
+ end
52
+
53
+ klass
54
+ end
55
+
56
+ def define_audited_class(klass)
57
+ default_audit_class = Audited.audit_class
58
+
59
+ Audited.audit_class = 'Motor::Audit'
60
+
61
+ klass.audited
62
+
63
+ klass
64
+ ensure
65
+ Audited.audit_class = default_audit_class
66
+ end
67
+
68
+ def define_class_properties(klass, model)
69
+ define_audited_class(klass) unless [Audited::Audit, Motor::Audit].include?(model)
70
+
71
+ klass.table_name = model.table_name
72
+
73
+ klass.instance_variable_set(:@__motor_model_name, model.name)
74
+
75
+ klass.instance_eval do
76
+ def name
77
+ @__motor_model_name
78
+ end
79
+
80
+ def inspect
81
+ super.gsub(/\#<Class:0x\w+>/, name)
82
+ end
83
+
84
+ def to_s
85
+ super.gsub(/\#<Class:0x\w+>/, name)
86
+ end
87
+
88
+ def anonymous?
89
+ true
90
+ end
91
+ end
92
+
93
+ klass
94
+ end
95
+
96
+ def define_searchable_columns_method(klass, config)
97
+ return if config[:searchable_columns].blank?
98
+
99
+ klass.instance_variable_set(:@__motor_searchable_columns, config[:searchable_columns])
100
+
101
+ klass.instance_eval do
102
+ def motor_searchable_columns
103
+ @__motor_searchable_columns
104
+ end
105
+ end
106
+ end
107
+
108
+ def define_columns_hash(klass, config)
109
+ return klass if config[:custom_sql].blank?
110
+
111
+ columns = Resources::CustomSqlColumnsCache.call(config[:custom_sql])
112
+
113
+ columns_hash =
114
+ columns.each_with_object({}) do |column, acc|
115
+ acc[column[:name]] =
116
+ ActiveRecord::ConnectionAdapters::Column.new(
117
+ column[:name],
118
+ nil,
119
+ ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: column[:column_type],
120
+ type: column[:column_type].to_sym)
121
+ )
122
+ end
123
+
124
+ klass.instance_variable_set(:@__motor_custom_sql_columns_hash, columns_hash)
125
+
126
+ # rubocop:disable Naming/MemoizedInstanceVariableName
127
+ klass.instance_eval do
128
+ def columns_hash
129
+ @__motor__columns_hash ||= @__motor_custom_sql_columns_hash.merge(super)
130
+ end
131
+ end
132
+ # rubocop:enable Naming/MemoizedInstanceVariableName
133
+ end
134
+
135
+ def define_column_reflections(klass, config)
136
+ config.fetch(:columns, []).each do |column|
137
+ reference = column[:reference]
138
+
139
+ next if reference.blank?
140
+
141
+ if reference[:reference_type] == 'belongs_to'
142
+ define_belongs_to_reflection(klass, reference)
143
+ else
144
+ define_has_one_reflection(klass, reference)
145
+ end
146
+ end
147
+ end
148
+
149
+ def define_belongs_to_reflection(klass, config)
150
+ klass.belongs_to(config[:name].to_sym,
151
+ class_name: config[:model_name]&.classify,
152
+ foreign_key: config[:foreign_key],
153
+ polymorphic: config[:polymorphic],
154
+ primary_key: config[:primary_key],
155
+ optional: true)
156
+ end
157
+
158
+ def define_has_one_reflection(klass, config)
159
+ if config[:model_name] == 'active_storage/attachment'
160
+ klass.has_one_attached(config[:name].delete_suffix('_attachment').to_sym)
161
+ else
162
+ options = {
163
+ **klass.reflections[config[:name]]&.options.to_h,
164
+ class_name: config[:model_name].classify,
165
+ foreign_key: config[:foreign_key],
166
+ primary_key: config[:primary_key],
167
+ **config[:options].to_h
168
+ }.symbolize_keys
169
+
170
+ klass.has_one(config[:name].to_sym, **options)
171
+ end
172
+ end
173
+
174
+ def configure_reflection_classes(klass, cache_key)
175
+ klass._reflections.each do |key, ref|
176
+ next unless configure_reflection_class?(ref)
177
+
178
+ ref_dup = ref.dup
179
+
180
+ if ref.klass.name == klass.name
181
+ ref_dup.instance_variable_set(:@klass, klass)
182
+ else
183
+ ref_dup.instance_variable_set(:@klass, call(ref.klass, cache_key: cache_key))
184
+ end
185
+
186
+ klass.reflections[key] = ref_dup
187
+ end
188
+
189
+ klass._reflections = klass.reflections
190
+
191
+ klass
192
+ end
193
+
194
+ def define_associations(klass, config)
195
+ config.fetch(:associations, []).each do |association|
196
+ next unless association[:virtual]
197
+
198
+ options = normalize_association_params(association)
199
+
200
+ filters = options.delete(:filters)
201
+
202
+ define_association(klass, association[:name], options, filters)
203
+ end
204
+ end
205
+
206
+ def define_association(klass, name, options, filters)
207
+ if options[:class_name] == 'ActiveStorage::Attachment'
208
+ klass.has_many_attached name.delete_suffix('_attachments').to_sym
209
+ elsif filters.present?
210
+ klass.has_many(name.to_sym, lambda {
211
+ Motor::ApiQuery::Filter.apply_filters(all, filters).distinct
212
+ }, **options.symbolize_keys)
213
+ else
214
+ klass.has_many(name.to_sym, **options.symbolize_keys)
215
+ end
216
+ end
217
+
218
+ def normalize_association_params(params)
219
+ options = params.slice(:foreign_key, :primary_key).merge(dependent: :destroy)
220
+
221
+ options[:class_name] = params[:model_name].classify
222
+ options[:as] = params[:foreign_key].delete_suffix('_id') if params[:polymorphic]
223
+
224
+ options.merge(params[:options] || {})
225
+ end
226
+
227
+ def maybe_fetch_from_cache(model, cache_key, miss_cache_block, postprocess_block)
228
+ return miss_cache_block.call unless cache_key
229
+
230
+ if CACHE_HASH[model.name] && CACHE_HASH[model.name][:key] == cache_key
231
+ CACHE_HASH[model.name][:value]
232
+ else
233
+ result = miss_cache_block.call
234
+
235
+ CACHE_HASH[model.name] = { key: cache_key, value: result }
236
+
237
+ postprocess_block.call(result)
238
+ end
239
+ end
240
+
241
+ def build_configured_model_from_configs(model, configs)
242
+ resource_config = configs.find { |r| r.name == model.name.underscore }
243
+
244
+ if resource_config
245
+ build_configured_model(model, resource_config.preferences)
246
+ else
247
+ define_class_properties(Class.new(model), model)
248
+ end
249
+ end
250
+
251
+ def configure_reflection_class?(ref)
252
+ begin
253
+ return false unless ref.klass
254
+ rescue StandardError
255
+ return false
256
+ end
257
+
258
+ return false if ref.klass.anonymous?
259
+ return false if ref.klass.name.demodulize.starts_with?(HAS_AND_BELONGS_TO_MANY_JOIN_MODEL_PREFIX)
260
+
261
+ true
262
+ end
263
+
264
+ def sti_model?(model)
265
+ !model.superclass.abstract_class && model.columns_hash[model.inheritance_column.to_s]
266
+ end
267
+ end
268
+ end
269
+ end