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,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Admin < ::Rails::Engine
5
+ config.custom_html = ''
6
+
7
+ ActiveSupport.cache_format_version = Rails.version.to_f
8
+
9
+ if !Motor.development? && Rails.env.development?
10
+ config.eager_load_paths.delete(File.expand_path('../../app/controllers', __dir__))
11
+ config.eager_load_paths.delete(File.expand_path('../../app/controllers/concerns', __dir__))
12
+ config.eager_load_paths.delete(File.expand_path('../../app/models', __dir__))
13
+
14
+ config.autoload_once_paths << File.expand_path('../../app/models', __dir__)
15
+ config.autoload_once_paths << File.expand_path('../../app/controllers', __dir__)
16
+ config.autoload_once_paths << File.expand_path('../../app/controllers/concerns', __dir__)
17
+ end
18
+
19
+ initializer 'motor.startup_message' do
20
+ config.after_initialize do
21
+ next unless Motor.server?
22
+
23
+ Rails.application.reload_routes!
24
+
25
+ if Rails.application.routes.url_helpers.respond_to?(:motor_admin_path)
26
+ url =
27
+ begin
28
+ Rails.application.routes.url_helpers.motor_admin_url
29
+ rescue ArgumentError
30
+ Rails.application.routes.url_helpers.motor_admin_path
31
+ end
32
+
33
+ puts
34
+ puts "⚡ Motor::Admin is starting under #{url}"
35
+ else
36
+ puts
37
+ puts '⚠️ Motor::Admin is not mounted.'
38
+ puts 'Add the following line to your config/routes.rb:'
39
+ puts
40
+ puts " mount Motor::Admin => '/admin'"
41
+ end
42
+
43
+ puts
44
+ end
45
+ end
46
+
47
+ initializer 'motor.configs.sync_middleware' do
48
+ next if Motor::Configs::SYNC_ACCESS_KEY.blank?
49
+
50
+ require 'motor/configs/sync_middleware'
51
+
52
+ Rails.application.config.middleware.insert_after(Rails::Rack::Logger, Motor::Configs::SyncMiddleware)
53
+ end
54
+
55
+ initializer 'motor.filter_params' do
56
+ Rails.application.config.filter_parameters += [/\Aio\z/]
57
+ end
58
+
59
+ initializer 'motor.alerts.scheduler' do
60
+ config.after_initialize do
61
+ next unless Motor.server?
62
+ next unless Rails.env.production?
63
+
64
+ Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
65
+ Motor::Notes::RemindersScheduler::SCHEDULER_TASK.execute
66
+ end
67
+ end
68
+
69
+ initializer 'motor.basic_auth' do
70
+ next if ENV['MOTOR_AUTH_PASSWORD'].blank?
71
+
72
+ config.middleware.use Rack::Auth::Basic do |username, password|
73
+ ActiveSupport::SecurityUtils.secure_compare(
74
+ ::Digest::SHA256.hexdigest(username),
75
+ ::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_USERNAME'].to_s)
76
+ ) &
77
+ ActiveSupport::SecurityUtils.secure_compare(
78
+ ::Digest::SHA256.hexdigest(password),
79
+ ::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_PASSWORD'].to_s)
80
+ )
81
+ end
82
+ end
83
+
84
+ initializer 'warden.configure.dispatch_requests' do
85
+ next unless defined?(Warden::JWTAuth)
86
+
87
+ config.after_initialize do
88
+ Warden::JWTAuth.configure do |config|
89
+ config.dispatch_requests += [
90
+ ['POST', /\A#{Regexp.escape(Motor::Admin.routes.url_helpers.motor_api_auth_tokens_path)}\z/]
91
+ ]
92
+ end
93
+ end
94
+ end
95
+
96
+ initializer 'action_cable.connection_class' do
97
+ config.after_initialize do
98
+ next if defined?(::ApplicationCable::Connection)
99
+ next unless defined?(::ActionCable)
100
+
101
+ ActionCable.server.config.connection_class = -> { Motor::ApplicationCable::Connection }
102
+ end
103
+ end
104
+
105
+ initializer 'motor.active_storage.extensions' do
106
+ config.after_initialize do
107
+ next unless defined?(ActiveStorage::Engine)
108
+
109
+ ActiveSupport.on_load(:active_storage_attachment) do
110
+ ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
111
+ end
112
+
113
+ ActiveSupport.on_load(:active_storage_blob) do
114
+ ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
115
+ end
116
+
117
+ ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
118
+ ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
119
+ end
120
+ end
121
+
122
+ initializer 'motor.upgrade' do
123
+ ActiveSupport.on_load(:motor_query) do
124
+ next unless Motor.server?
125
+
126
+ unless Motor::Query.table_exists?
127
+ puts
128
+ puts ' => Run `rails g motor:install && rake db:migrate` in order to create Motor Admin configuration tables'
129
+ puts
130
+ end
131
+
132
+ if !Motor::ApiConfig.table_exists? || !Motor::Note.table_exists?
133
+ puts
134
+ puts ' => Run `rails g motor:upgrade && rake db:migrate` ' \
135
+ 'to perform data migration and enable the latest features'
136
+ puts
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module Persistance
6
+ ALERT_ATTRIBUTES = %i[
7
+ query_id
8
+ name
9
+ description
10
+ preferences
11
+ is_enabled
12
+ to_emails
13
+ ].freeze
14
+
15
+ NameAlreadyExists = Class.new(StandardError)
16
+ InvalidInterval = Class.new(StandardError)
17
+
18
+ NORMALIZE_INTERVAL_REGEXP = /\A(?:every\s+)?/i.freeze
19
+
20
+ module_function
21
+
22
+ def build_from_params(params, current_user = nil)
23
+ alert = assign_attributes(Alert.new, params)
24
+
25
+ alert.author = current_user
26
+
27
+ alert
28
+ end
29
+
30
+ def create_from_params!(params, current_user = nil)
31
+ raise NameAlreadyExists if Alert.exists?(name: params[:name])
32
+
33
+ alert = build_from_params(params, current_user)
34
+
35
+ raise InvalidInterval unless alert.cron
36
+
37
+ ApplicationRecord.transaction do
38
+ alert.save!
39
+ end
40
+
41
+ alert
42
+ rescue ActiveRecord::RecordNotUnique
43
+ retry
44
+ end
45
+
46
+ def update_from_params!(alert, params, force_replace: false)
47
+ tag_ids = alert.tags.ids
48
+
49
+ alert = assign_attributes(alert, params)
50
+
51
+ raise NameAlreadyExists if !force_replace && name_already_exists?(alert)
52
+ raise InvalidInterval unless alert.cron
53
+
54
+ ApplicationRecord.transaction do
55
+ archive_with_existing_name(alert) if force_replace
56
+
57
+ alert.save!
58
+ end
59
+
60
+ alert.touch if tags_changed?(tag_ids, alert) && params[:updated_at].blank?
61
+
62
+ alert
63
+ rescue ActiveRecord::RecordNotUnique
64
+ retry
65
+ end
66
+
67
+ def tags_changed?(previous_ids, alert)
68
+ previous_ids.sort != alert.tags.reload.ids.sort
69
+ end
70
+
71
+ def assign_attributes(alert, params)
72
+ alert.assign_attributes(params.slice(*ALERT_ATTRIBUTES))
73
+ alert.preferences[:interval] = normalize_interval(alert.preferences[:interval])
74
+ alert.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
75
+
76
+ Motor::Tags.assign_tags(alert, params[:tags])
77
+ end
78
+
79
+ def archive_with_existing_name(alert)
80
+ Motor::Alert.where(['name = ? AND id != ?', alert.name, alert.id])
81
+ .update_all(deleted_at: Time.current)
82
+ end
83
+
84
+ def normalize_interval(interval)
85
+ interval.to_s.gsub(NORMALIZE_INTERVAL_REGEXP, 'every ')
86
+ end
87
+
88
+ def name_already_exists?(alert)
89
+ if alert.new_record?
90
+ Alert.exists?(name: alert.name, deleted_at: nil)
91
+ else
92
+ Alert.exists?(['name = ? AND id != ? AND deleted_at IS NULL', alert.name, alert.id])
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module ScheduledAlertsCache
6
+ CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
7
+
8
+ module_function
9
+
10
+ def all
11
+ ActiveRecord::Base.logger.silence do
12
+ cache_key = Motor::Alert.maximum(:updated_at)
13
+
14
+ return [] if cache_key.nil?
15
+
16
+ CACHE_STORE.fetch(cache_key) do
17
+ clear
18
+
19
+ Motor::Alert.all.active.enabled.to_a
20
+ end
21
+ end
22
+ end
23
+
24
+ def clear
25
+ CACHE_STORE.clear
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module Scheduler
6
+ SCHEDULER_INTERVAL = 1.minute
7
+ CHECK_BEHIND_DURATION = 15.minutes
8
+
9
+ SCHEDULER_TASK = Concurrent::TimerTask.new(
10
+ execution_interval: SCHEDULER_INTERVAL
11
+ ) { Motor::Alerts::Scheduler.call }
12
+
13
+ ALREADY_PROCESSED_CACHE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
14
+
15
+ module_function
16
+
17
+ def call
18
+ ScheduledAlertsCache.find_each do |alert|
19
+ next unless (CHECK_BEHIND_DURATION.ago..Time.current).cover?(alert.cron.previous_time.to_local_time)
20
+
21
+ ALREADY_PROCESSED_CACHE.fetch("#{alert.id}-#{alert.cron.previous_time.to_i}") do
22
+ Motor::AlertSendingJob.perform_later(alert).job_id
23
+ end
24
+ rescue StandardError => e
25
+ Rails.logger.error(e)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module SlackSender
6
+ MARKDOWN_LINK_TRANSFORM_REGEXP = /\[(.+?)\]\((.+?)\)/.freeze
7
+ MARKDOWN_PATHS_TRANSFORM_REGEXP = /\n\s*\#{1,5}\s*(.*)\n?/.freeze
8
+ MARKDOWN_HEADING_TRANSFORM_REGEXP = /\n\s*\#{1,5}\s*(.*)\n?/.freeze
9
+ MARKDOWN_BOLD_TRANSFORM_REGEXP = /[*_]{2}/.freeze
10
+
11
+ DEFAULT_FILENAME = 'data.csv'
12
+
13
+ module_function
14
+
15
+ def call(alert, conversation_id, slack_user: nil)
16
+ query_result = load_data(alert, slack_user)
17
+
18
+ if alert.query.preferences[:visualization] == 'markdown'
19
+ text = [build_query_link(alert), render_markdown(alert, query_result)].join
20
+
21
+ Slack::Client.send_message(channel: conversation_id,
22
+ as_user: slack_user.present?,
23
+ text: text)
24
+ else
25
+ Slack::Client.send_message(channel: conversation_id,
26
+ as_user: slack_user.present?,
27
+ text: build_alert_message(alert))
28
+ Slack::Client.send_file(channels: conversation_id,
29
+ content: generate_csv(query_result),
30
+ filename: DEFAULT_FILENAME)
31
+ end
32
+ end
33
+
34
+ def build_alert_message(alert)
35
+ <<~MARKDOWN
36
+ #{build_query_link(alert)}
37
+ #{alert.description}
38
+ MARKDOWN
39
+ end
40
+
41
+ def build_query_link(alert)
42
+ link = Motor::Admin.routes.url_helpers.motor_ui_query_url(alert.query_id, host: Motor.app_host)
43
+
44
+ "*<#{link}|#{alert.name}>*"
45
+ end
46
+
47
+ def render_markdown(alert, query_result)
48
+ params = query_result.columns.pluck(:name).zip(query_result.data[0]).to_h.symbolize_keys
49
+
50
+ markdown =
51
+ ["\n", Mustache.render(alert.query.preferences[:visualization_options][:markdown], **params)].join
52
+
53
+ markdown.gsub(/\((.*)\)/, "(http://#{Motor.app_host}\\1)")
54
+ .gsub(MARKDOWN_LINK_TRANSFORM_REGEXP, '<\2|\1>')
55
+ .gsub(MARKDOWN_HEADING_TRANSFORM_REGEXP, "\n\n*\\1*")
56
+ .gsub(MARKDOWN_BOLD_TRANSFORM_REGEXP, '*')
57
+ end
58
+
59
+ def generate_csv(query_result)
60
+ rows = [query_result.columns.pluck(:name)] + query_result.data
61
+
62
+ rows.map(&:to_csv).join
63
+ end
64
+
65
+ def load_data(alert, slack_user = nil)
66
+ if slack_user
67
+ Queries::RunQuery.call(alert.query, variables_hash: { current_user_email: slack_user['email'] })
68
+ else
69
+ Queries::RunQuery.call(alert.query)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module_function
6
+
7
+ def send_alert(alert)
8
+ if alert.preferences[:adapter] == 'slack'
9
+ send_slack_alert(alert)
10
+ else
11
+ send_email_alert(alert)
12
+ end
13
+ end
14
+
15
+ def send_email_alert(alert)
16
+ alert.to_emails.split(',').each do |email|
17
+ if email.include?('@')
18
+ Motor::AlertsMailer.alert_email(alert, email: email).deliver_now!
19
+ else
20
+ send_query_email_alerts(alert, email.gsub(/[{}]/, ''))
21
+ end
22
+ end
23
+ end
24
+
25
+ def send_query_email_alerts(alert, column_name)
26
+ result = Queries::RunQuery.call(alert.query)
27
+
28
+ column_index = result.columns.find_index { |c| c[:name] == column_name }
29
+
30
+ emails = result.data.pluck(column_index).uniq
31
+
32
+ emails.each do |email|
33
+ Motor::AlertsMailer.alert_email(alert, email: email).deliver_now!
34
+ end
35
+ end
36
+
37
+ def send_slack_alert(alert)
38
+ slack_users = Slack::Client.load_users(limit: Slack::ITEMS_LIMIT)['members']
39
+
40
+ alert.preferences[:slack_conversation_ids].each do |conversation_id|
41
+ user = slack_users.find { |u| u['id'] == conversation_id }
42
+
43
+ Motor::Alerts::SlackSender.call(alert, conversation_id, slack_user: user)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ require_relative 'alerts/scheduler'
50
+ require_relative 'alerts/scheduled_alerts_cache'
51
+ require_relative 'alerts/persistance'
52
+ require_relative 'alerts/slack_sender'
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiConfigs
5
+ METHODS = %w[get post put delete].freeze
6
+
7
+ DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
8
+
9
+ InvalidHttpMethod = Class.new(StandardError)
10
+
11
+ module_function
12
+
13
+ def run(api_config, method: nil, path: nil, body: nil, params: {}, headers: {})
14
+ method ||= 'get'
15
+
16
+ raise InvalidHttpMethod unless METHODS.include?(method.downcase)
17
+
18
+ Motor::NetHttpUtils.public_send(
19
+ method.downcase.to_sym,
20
+ api_config.url.to_s.sub(%r{/?\z}, '/') + path.delete_prefix('/'),
21
+ params,
22
+ DEFAULT_HEADERS.merge(headers).merge(api_config.headers),
23
+ body&.to_json
24
+ )
25
+ end
26
+
27
+ def run_grapql(api_config, query:, variables: {}, headers: {})
28
+ body = {
29
+ query: query,
30
+ variables: variables.merge(form_data: variables)
31
+ }
32
+
33
+ Motor::NetHttpUtils.post(
34
+ api_config.url,
35
+ {},
36
+ DEFAULT_HEADERS.merge(headers).merge(api_config.headers),
37
+ body.to_json
38
+ )
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module ApplyScope
6
+ module_function
7
+
8
+ def call(rel, scope)
9
+ return rel if scope.blank?
10
+
11
+ scope_symbol = scope.to_sym
12
+
13
+ if rel.klass.defined_scopes.include?(scope_symbol)
14
+ rel.public_send(scope_symbol)
15
+ else
16
+ apply_filter_scope(rel, scope)
17
+ end
18
+ end
19
+
20
+ def apply_filter_scope(rel, scope)
21
+ configs = Motor::Resource.find_by(name: rel.klass.name.underscore)
22
+
23
+ return rel unless configs
24
+
25
+ scope_configs = configs.preferences[:scopes].find { |s| s[:name] == scope }
26
+
27
+ return rel unless scope_configs
28
+ return rel unless scope_configs[:preferences]
29
+
30
+ rel = ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
31
+
32
+ apply_order(rel, scope_configs[:preferences][:sort])
33
+ end
34
+
35
+ def apply_order(rel, params)
36
+ return rel if params.blank?
37
+
38
+ sort_key, sort_order = params.values_at(:key, :order)
39
+
40
+ rel.order(sort_key => sort_order)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module BuildJson
6
+ module_function
7
+
8
+ # @param rel [ActiveRecord::Base, ActiveRecord::Relation]
9
+ # @param params [Hash]
10
+ # @param current_ability [CanCan::Ability]
11
+ # @return [Hash]
12
+ def call(rel, params, current_ability = Motor::CancanUtils::CanManageAll.new)
13
+ rel = rel.none if limit_zero_params?(params)
14
+ rel = rel.preload_associations_lazily if rel.is_a?(ActiveRecord::Relation)
15
+
16
+ model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
17
+
18
+ include_hash = build_include_hash(params['include'])
19
+ models_index = build_models_index(model, include_hash)
20
+
21
+ json_params = normalize_include_params(include_hash)
22
+
23
+ assign_fields_params!(json_params, model, params, current_ability, models_index)
24
+
25
+ rel.as_json(json_params.with_indifferent_access)
26
+ end
27
+
28
+ # @param include_params [Hash]
29
+ # @return [Hash]
30
+ def build_include_hash(include_params)
31
+ return {} if include_params.blank?
32
+
33
+ if include_params.is_a?(String)
34
+ build_hash_from_string_path(include_params)
35
+ else
36
+ include_params
37
+ end
38
+ end
39
+
40
+ # @param json_params [Hash]
41
+ # @param model [Class<ActiveRecord::Base>]
42
+ # @param params [Hash]
43
+ # @param current_ability [CanCan::Ability]
44
+ # @param models_index [Hash]
45
+ # @return [void]
46
+ def assign_fields_params!(json_params, model, params, current_ability, models_index)
47
+ return if params[:fields].blank?
48
+
49
+ params[:fields].each do |key, fields|
50
+ fields_model = models_index[key]
51
+
52
+ next unless model
53
+
54
+ fields = fields.split(',') if fields.is_a?(String)
55
+
56
+ fields_hash = fields_model == model ? json_params : find_key_in_params(json_params, key)
57
+
58
+ fields_hash.merge!(build_fields_hash(fields_model, fields, current_ability))
59
+ end
60
+ end
61
+
62
+ # @param model [Class<ActiveRecord::Base>]
63
+ # @param fields [Hash]
64
+ # @param current_ability [CanCan::Ability]
65
+ # @return [Hash]
66
+ def build_fields_hash(model, fields, current_ability)
67
+ return { 'methods' => fields } unless model
68
+
69
+ column_names = model.column_names.map(&:to_sym)
70
+ instance_methods = model.instance_methods
71
+ permitted_attributes = current_ability.permitted_attributes(:read, model)
72
+ is_permitted_all = column_names == permitted_attributes
73
+
74
+ fields_hash = { 'only' => [], 'methods' => [] }
75
+
76
+ fields.each_with_object(fields_hash) do |field, acc|
77
+ field_symbol = field.to_sym
78
+
79
+ next if !is_permitted_all && permitted_attributes.exclude?(field_symbol)
80
+
81
+ if model.columns_hash[field]
82
+ acc['only'] << field
83
+ elsif instance_methods.include?(field_symbol)
84
+ acc['methods'] << field
85
+ end
86
+ end
87
+ end
88
+
89
+ # @param params [Hash]
90
+ # @param key [String]
91
+ # @return [Hash]
92
+ def find_key_in_params(params, key)
93
+ params = params['include']
94
+
95
+ return {} if params.blank?
96
+ return params[key] if params[key]
97
+
98
+ params.keys.reduce(nil) do |acc, k|
99
+ acc || find_key_in_params(params[k], key)
100
+ end
101
+ end
102
+
103
+ # @param params [Hash]
104
+ # @return [Hash]
105
+ def normalize_include_params(params)
106
+ case params
107
+ when Array
108
+ params.index_with do |_|
109
+ { 'include' => {} }
110
+ end
111
+ when String
112
+ { params => { 'include' => {} } }
113
+ when Hash
114
+ include_hash =
115
+ params.transform_values do |value|
116
+ normalize_include_params(value)
117
+ end
118
+
119
+ { 'include' => include_hash }
120
+ else
121
+ raise ArgumentError, "Wrong include param type #{params.class}"
122
+ end
123
+ end
124
+
125
+ # @param model [Class<ActiveRecord::Base>]
126
+ # @param includes_hash [Hash]
127
+ # @return [Hash]
128
+ def build_models_index(model, includes_hash)
129
+ default_index = {
130
+ model.name.underscore => model,
131
+ model.name.underscore.split('/').last => model
132
+ }
133
+
134
+ includes_hash.reduce(default_index) do |acc, (key, value)|
135
+ reflection = model.reflections[key]
136
+
137
+ next acc unless reflection
138
+ next acc if reflection.polymorphic?
139
+
140
+ acc[key] = reflection.klass
141
+
142
+ acc.merge(build_models_index(reflection.klass, value))
143
+ end
144
+ end
145
+
146
+ # @param string_path [String]
147
+ # @return [Hash]
148
+ def build_hash_from_string_path(string_path)
149
+ string_path.split(',').reduce({}) do |accumulator, path|
150
+ hash = {}
151
+
152
+ path.split('.').reduce(hash) do |acc, part|
153
+ acc_hash = {}
154
+
155
+ acc[part] = acc_hash
156
+
157
+ acc_hash
158
+ end
159
+
160
+ accumulator.deep_merge(hash)
161
+ end
162
+ end
163
+
164
+ # @param params [Hash]
165
+ # @return [Boolean]
166
+ def limit_zero_params?(params)
167
+ params.dig(:page, :limit).then { |limit| limit.present? && limit.to_i.zero? }
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module BuildMeta
6
+ module_function
7
+
8
+ def call(rel, params)
9
+ meta = {}
10
+
11
+ if params[:meta].to_s.include?('count')
12
+ meta[:count] =
13
+ rel.limit(nil).offset(nil).reorder(nil).select(rel.klass.arel_table[rel.klass.primary_key]).count
14
+ end
15
+
16
+ meta
17
+ end
18
+ end
19
+ end
20
+ end