motor-admin-cstham8 0.4.35

Sign up to get free protection for your applications and to get access to all the features.
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