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,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module BuildUiAppTag
6
+ CACHE_STORE =
7
+ if Motor.development?
8
+ ActiveSupport::Cache::NullStore.new
9
+ else
10
+ ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
11
+ end
12
+
13
+ module_function
14
+
15
+ def call(current_user = nil, current_ability = nil, cache_keys: LoadFromCache.load_cache_keys)
16
+ CACHE_STORE.fetch(app_tag_cache_key(cache_keys, current_user, current_ability)) do
17
+ CACHE_STORE.clear
18
+
19
+ data = build_data(cache_keys, current_user, current_ability)
20
+ Motor::ApplicationController.helpers.tag.div('', id: 'app', data: data)
21
+ end
22
+ end
23
+
24
+ def app_tag_cache_key(cache_keys, current_user, current_ability)
25
+ key = "#{I18n.locale}#{cache_keys.hash}#{current_user&.id}#{current_ability&.rules_hash}"
26
+
27
+ key += Motor::DefineArModels.defined_models_schema_md5.to_s if defined?(Motor::DefineArModels)
28
+
29
+ if Rails.env.development?
30
+ key += Digest::MD5.hexdigest(ActiveRecord::Base.descendants.map(&:object_id).sort.join)
31
+ end
32
+
33
+ key
34
+ end
35
+
36
+ # rubocop:disable Metrics/AbcSize
37
+ # rubocop:disable Metrics/MethodLength
38
+ # @return [Hash]
39
+ def build_data(cache_keys = {}, current_user = nil, current_ability = nil)
40
+ configs_cache_key = cache_keys[:configs]
41
+
42
+ { version: Motor::VERSION,
43
+ current_user: serialize_current_user(current_user),
44
+ current_rules: current_ability.serialized_rules,
45
+ audits_count: Motor::Audit.count,
46
+ i18n: i18n_data,
47
+ base_path: Motor::Admin.routes.url_helpers.motor_path,
48
+ cable_path: Motor::Admin.routes.url_helpers.try(:motor_cable_path),
49
+ admin_settings_path: Rails.application.routes.url_helpers.try(:admin_settings_general_path),
50
+ active_storage_direct_uploads_enabled: ENV['MOTOR_ACTIVE_STORAGE_DIRECT_UPLOADS_ENABLED'].present?,
51
+ schema: Motor::BuildSchema.call(cache_keys, current_ability),
52
+ header_links: header_links_data_hash(current_user, current_ability, configs_cache_key),
53
+ homepage_layout: homepage_layout_data_hash(configs_cache_key),
54
+ databases: database_names,
55
+ queries: queries_data_hash(build_cache_key(cache_keys, :queries, current_user, current_ability),
56
+ current_ability),
57
+ dashboards: dashboards_data_hash(build_cache_key(cache_keys, :dashboards, current_user, current_ability),
58
+ current_ability),
59
+ alerts: alerts_data_hash(build_cache_key(cache_keys, :alerts, current_user, current_ability),
60
+ current_ability),
61
+ forms: forms_data_hash(build_cache_key(cache_keys, :forms, current_user, current_ability), current_ability) }
62
+ end
63
+ # rubocop:enable Metrics/AbcSize
64
+ # rubocop:enable Metrics/MethodLength
65
+
66
+ def i18n_data
67
+ I18n.t('motor', default: I18n.t('motor', locale: :en))
68
+ end
69
+
70
+ def serialize_current_user(user)
71
+ return unless user
72
+
73
+ attrs = user.as_json(only: %i[id email first_name last_name])
74
+
75
+ attrs['role'] = user.role if user.respond_to?(:role)
76
+ attrs['role_names'] = user.role_names if user.respond_to?(:role_names)
77
+
78
+ attrs
79
+ end
80
+
81
+ # @return [String]
82
+ def build_cache_key(cache_keys, key, current_user, current_ability)
83
+ "#{cache_keys[key].hash}#{current_user&.id}#{current_ability&.rules_hash}"
84
+ end
85
+
86
+ def header_links_data_hash(current_user, current_ability, cache_key = nil)
87
+ configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
88
+
89
+ links = configs.find { |c| c.key == 'header.links' }&.value || []
90
+ links = add_default_links(links)
91
+
92
+ return links unless current_user
93
+ return links if current_ability.can?(:manage, :all)
94
+
95
+ filter_links_for_user(current_user, links)
96
+ end
97
+
98
+ def add_default_links(links)
99
+ new_links = links.clone
100
+
101
+ unless links.find { |l| l['link_type'] == 'forms' }
102
+ new_links.unshift({ 'name' => I18n.t('motor.forms'), 'link_type' => 'forms' })
103
+ end
104
+
105
+ unless links.find { |l| l['link_type'] == 'reports' }
106
+ new_links.unshift({ 'name' => I18n.t('motor.reports'), 'link_type' => 'reports' })
107
+ end
108
+
109
+ new_links
110
+ end
111
+
112
+ def filter_links_for_user(current_user, links)
113
+ links.select do |link|
114
+ conditions = link['conditions']
115
+
116
+ next true if conditions.blank?
117
+
118
+ conditions.all? do |cond|
119
+ field_name =
120
+ if cond['field'] == 'role' && !current_user.respond_to?(:role)
121
+ :role_names
122
+ else
123
+ cond['field'].to_sym
124
+ end
125
+
126
+ next false unless field_name.in?(%i[email role role_names])
127
+ next false unless current_user.respond_to?(field_name)
128
+
129
+ Array.wrap(current_user.public_send(field_name)).intersection(Array.wrap(cond['value'])).present?
130
+ end
131
+ end
132
+ end
133
+
134
+ def homepage_layout_data_hash(cache_key = nil)
135
+ configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
136
+
137
+ configs.find { |c| c.key == 'homepage.layout' }&.value || []
138
+ end
139
+
140
+ def database_names
141
+ if defined?(Motor::EncryptedConfig)
142
+ Motor::DatabaseClasses.constants.map { |e| e.to_s.titleize }
143
+ elsif ActiveRecord::Base.configurations.try(:configurations)
144
+ ActiveRecord::Base.configurations.configurations
145
+ .select { |c| c.env_name == Rails.env }
146
+ .map { |c| c.try(:name) || c.try(:spec_name) }.compact
147
+ else
148
+ ['primary']
149
+ end
150
+ end
151
+
152
+ def queries_data_hash(cache_key = nil, current_ability = nil)
153
+ Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key, current_ability: current_ability)
154
+ .as_json(only: %i[id name updated_at],
155
+ include: { tags: { only: %i[id name] } })
156
+ end
157
+
158
+ def dashboards_data_hash(cache_key = nil, current_ability = nil)
159
+ Motor::Configs::LoadFromCache.load_dashboards(cache_key: cache_key, current_ability: current_ability)
160
+ .as_json(only: %i[id title updated_at],
161
+ include: { tags: { only: %i[id name] } })
162
+ end
163
+
164
+ def alerts_data_hash(cache_key = nil, current_ability = nil)
165
+ Motor::Configs::LoadFromCache.load_alerts(cache_key: cache_key, current_ability: current_ability)
166
+ .as_json(only: %i[id name is_enabled updated_at],
167
+ include: { tags: { only: %i[id name] } })
168
+ end
169
+
170
+ def forms_data_hash(cache_key = nil, current_ability = nil)
171
+ Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key, current_ability: current_ability)
172
+ .as_json(only: %i[id name updated_at],
173
+ include: { tags: { only: %i[id name] } })
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module LoadFromCache
6
+ CACHE_HASH = ActiveSupport::HashWithIndifferentAccess.new
7
+
8
+ module_function
9
+
10
+ def call
11
+ cache_keys = load_cache_keys
12
+
13
+ {
14
+ configs: load_configs(cache_key: cache_keys[:configs]),
15
+ resources: load_resources(cache_key: cache_keys[:resources]),
16
+ queries: load_queries(cache_key: cache_keys[:queries]),
17
+ dashboards: load_dashboards(cache_key: cache_keys[:dashboards]),
18
+ alerts: load_alerts(cache_key: cache_keys[:alerts]),
19
+ forms: load_forms(cache_key: cache_keys[:forms])
20
+ }
21
+ end
22
+
23
+ def load_configs(cache_key: nil)
24
+ maybe_fetch_from_cache('configs', cache_key) do
25
+ Motor::Config.all.load
26
+ end
27
+ end
28
+
29
+ def load_resources(cache_key: nil)
30
+ maybe_fetch_from_cache('resources', cache_key) do
31
+ Motor::Resource.all.load
32
+ end
33
+ end
34
+
35
+ def load_queries(cache_key: nil, current_ability: nil)
36
+ maybe_fetch_from_cache('queries', cache_key) do
37
+ rel = Motor::Query.all.active.preload(:tags)
38
+ rel = rel.accessible_by(current_ability) if current_ability
39
+
40
+ rel.load
41
+ end
42
+ end
43
+
44
+ def load_dashboards(cache_key: nil, current_ability: nil)
45
+ maybe_fetch_from_cache('dashboards', cache_key) do
46
+ rel = Motor::Dashboard.all.active.preload(:tags)
47
+ rel = rel.accessible_by(current_ability) if current_ability
48
+
49
+ rel.load
50
+ end
51
+ end
52
+
53
+ def load_alerts(cache_key: nil, current_ability: nil)
54
+ maybe_fetch_from_cache('alerts', cache_key) do
55
+ rel = Motor::Alert.all.active.preload(:tags)
56
+ rel = rel.accessible_by(current_ability) if current_ability
57
+
58
+ rel.load
59
+ end
60
+ end
61
+
62
+ def load_forms(cache_key: nil, current_ability: nil)
63
+ maybe_fetch_from_cache('forms', cache_key) do
64
+ rel = Motor::Form.all.active.preload(:tags)
65
+ rel = rel.accessible_by(current_ability) if current_ability
66
+
67
+ rel.load
68
+ end
69
+ end
70
+
71
+ def load_api_configs(cache_key: nil)
72
+ maybe_fetch_from_cache('forms', cache_key) do
73
+ Motor::ApiConfig.all.active.load
74
+ end
75
+ end
76
+
77
+ def maybe_fetch_from_cache(type, cache_key)
78
+ return yield unless cache_key
79
+
80
+ if CACHE_HASH[type] && CACHE_HASH[type][:key] == cache_key
81
+ CACHE_HASH[type][:value]
82
+ else
83
+ result = yield
84
+
85
+ CACHE_HASH[type] = { key: cache_key, value: result }
86
+
87
+ result
88
+ end
89
+ end
90
+
91
+ def load_cache_keys
92
+ result = ActiveRecord::Base.connection.exec_query(cache_keys_sql).rows
93
+
94
+ result.to_h.with_indifferent_access
95
+ end
96
+
97
+ def cache_keys_sql
98
+ [
99
+ Motor::Config.select("'configs', MAX(updated_at)").to_sql,
100
+ Motor::Resource.select("'resources', MAX(updated_at)").to_sql,
101
+ Motor::Dashboard.select("'dashboards', MAX(updated_at)").to_sql,
102
+ Motor::Alert.select("'alerts', MAX(updated_at)").to_sql,
103
+ Motor::Query.select("'queries', MAX(updated_at)").to_sql,
104
+ Motor::Form.select("'forms', MAX(updated_at)").to_sql,
105
+ Motor::ApiConfig.select("'api_configs', MAX(updated_at)").to_sql
106
+ ].join(' UNION ')
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module SyncFromFile
6
+ MUTEXT = Mutex.new
7
+ FILE_TIMESTAMPS_STORE = ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte)
8
+
9
+ module_function
10
+
11
+ def call(with_exception: false)
12
+ MUTEXT.synchronize do
13
+ file = Pathname.new(Motor::Configs.file_path)
14
+
15
+ file_timestamp =
16
+ begin
17
+ file.ctime
18
+ rescue Errno::ENOENT
19
+ raise if with_exception
20
+
21
+ nil
22
+ end
23
+
24
+ next unless file_timestamp
25
+
26
+ FILE_TIMESTAMPS_STORE.fetch(file_timestamp.to_s) do
27
+ Motor::Configs::SyncFromHash.call(
28
+ YAML.safe_load(file.read, permitted_classes: [Time, Date])
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module SyncFromHash
6
+ module_function
7
+
8
+ def call(configs_hash)
9
+ return if configs_hash.blank?
10
+
11
+ configs_hash = configs_hash.with_indifferent_access
12
+
13
+ Motor::ApplicationRecord.transaction do
14
+ sync_queries(configs_hash)
15
+ sync_alerts(configs_hash)
16
+ sync_dashboards(configs_hash)
17
+ sync_forms(configs_hash)
18
+ sync_configs(configs_hash)
19
+ sync_resources(configs_hash)
20
+ sync_api_configs(configs_hash)
21
+ end
22
+ end
23
+
24
+ def sync_queries(configs_hash)
25
+ sync_taggable(
26
+ Motor::Configs::LoadFromCache.load_queries,
27
+ configs_hash[:queries],
28
+ configs_hash[:file_version],
29
+ Motor::Queries::Persistance.method(:update_from_params!)
30
+ )
31
+ end
32
+
33
+ def sync_alerts(configs_hash)
34
+ sync_taggable(
35
+ Motor::Configs::LoadFromCache.load_alerts,
36
+ configs_hash[:alerts],
37
+ configs_hash[:file_version],
38
+ Motor::Alerts::Persistance.method(:update_from_params!)
39
+ )
40
+ end
41
+
42
+ def sync_forms(configs_hash)
43
+ sync_taggable(
44
+ Motor::Configs::LoadFromCache.load_forms,
45
+ configs_hash[:forms],
46
+ configs_hash[:file_version],
47
+ Motor::Forms::Persistance.method(:update_from_params!)
48
+ )
49
+ end
50
+
51
+ def sync_dashboards(configs_hash)
52
+ sync_taggable(
53
+ Motor::Configs::LoadFromCache.load_dashboards,
54
+ configs_hash[:dashboards],
55
+ configs_hash[:file_version],
56
+ Motor::Dashboards::Persistance.method(:update_from_params!)
57
+ )
58
+ end
59
+
60
+ def sync_configs(configs_hash)
61
+ configs_index = Motor::Configs::LoadFromCache.load_configs.index_by(&:key)
62
+
63
+ configs_hash[:configs].each do |attrs|
64
+ record = configs_index[attrs[:key]] || Motor::Config.new
65
+
66
+ next if record.updated_at && attrs[:updated_at] <= record.updated_at
67
+
68
+ record.update!(attrs)
69
+ end
70
+
71
+ ActiveRecordUtils.reset_id_sequence!(Motor::Config)
72
+ end
73
+
74
+ def sync_api_configs(configs_hash)
75
+ return if configs_hash[:api_configs].blank?
76
+
77
+ configs_index = Motor::Configs::LoadFromCache.load_api_configs.index_by(&:name)
78
+
79
+ configs_hash[:api_configs].each do |attrs|
80
+ record = configs_index[attrs[:name]] || Motor::ApiConfig.new
81
+
82
+ next if record.updated_at && attrs[:updated_at] <= record.updated_at
83
+
84
+ record.update!(attrs.merge(deleted_at: nil))
85
+ end
86
+
87
+ archive_api_configs(configs_index, configs_hash[:api_configs])
88
+
89
+ ActiveRecordUtils.reset_id_sequence!(Motor::ApiConfig)
90
+ end
91
+
92
+ def archive_api_configs(configs_index, api_configs)
93
+ configs_index.except(*api_configs.pluck('name')).each_value do |config|
94
+ config.update!(deleted_at: Time.current) if config.deleted_at.blank?
95
+ end
96
+ end
97
+
98
+ def sync_resources(configs_hash)
99
+ resources_index = Motor::Configs::LoadFromCache.load_resources.index_by(&:name)
100
+
101
+ configs_hash[:resources].each do |attrs|
102
+ record = resources_index.fetch(attrs[:name], Motor::Resource.new)
103
+
104
+ next if record.updated_at && attrs[:updated_at] < record.updated_at
105
+ next if record.updated_at &&
106
+ attrs[:updated_at] == record.updated_at &&
107
+ attrs[:preferences] == record.preferences
108
+
109
+ record.updated_at_will_change!
110
+ record.update!(attrs)
111
+ end
112
+
113
+ ActiveRecordUtils.reset_id_sequence!(Motor::Resource)
114
+ end
115
+
116
+ def sync_taggable(records, config_items, configs_timestamp, update_proc)
117
+ processed_records, create_items = update_taggable_items(records, config_items, update_proc)
118
+
119
+ create_taggable_items(create_items, records.klass, update_proc)
120
+
121
+ archive_taggable_items(records - processed_records, configs_timestamp)
122
+
123
+ ActiveRecordUtils.reset_id_sequence!(records.klass)
124
+ end
125
+
126
+ def update_taggable_items(records, config_items, update_proc)
127
+ record_ids_hash = records.index_by(&:id)
128
+
129
+ config_items.each_with_object([[], []]) do |attrs, (processed_acc, create_acc)|
130
+ record = record_ids_hash[attrs[:id]]
131
+
132
+ next create_acc << attrs unless record
133
+
134
+ processed_acc << record if record
135
+
136
+ next if record.updated_at >= attrs[:updated_at]
137
+
138
+ update_proc.call(record, attrs, force_replace: true)
139
+ end
140
+ end
141
+
142
+ def create_taggable_items(create_items, records_class, update_proc)
143
+ create_items.each do |attrs|
144
+ record = records_class.find_or_initialize_by(id: attrs[:id]).tap { |e| e.deleted_at = nil }
145
+
146
+ update_proc.call(record, attrs, force_replace: true)
147
+ end
148
+ end
149
+
150
+ def archive_taggable_items(records_to_remove, configs_timestamp)
151
+ records_to_remove.each do |record|
152
+ next if record.updated_at > configs_timestamp
153
+
154
+ record.update!(deleted_at: Time.current) if record.deleted_at.blank?
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ class SyncMiddleware
6
+ KeyNotSpecified = Class.new(StandardError)
7
+ NotAuthenticated = Class.new(StandardError)
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ if env['PATH_INFO'] == Motor::Configs::SYNC_API_PATH
15
+ authenticate!(env['HTTP_X_AUTHORIZATION'])
16
+
17
+ case env['REQUEST_METHOD']
18
+ when 'GET'
19
+ respond_with_configs
20
+ when 'POST'
21
+ input = env['rack.input']
22
+ input.rewind
23
+ sync_configs(input.read)
24
+ else
25
+ @app.call(env)
26
+ end
27
+ else
28
+ @app.call(env)
29
+ end
30
+ rescue NotAuthenticated
31
+ [403, {}, ['Invalid synchronization API key']]
32
+ rescue KeyNotSpecified
33
+ [404, {}, ['Set `MOTOR_SYNC_API_KEY` environment variable in order to sync configs']]
34
+ end
35
+
36
+ private
37
+
38
+ def authenticate!(token)
39
+ raise KeyNotSpecified if Motor::Configs::SYNC_ACCESS_KEY.blank?
40
+ raise NotAuthenticated if token.blank?
41
+
42
+ is_token_valid =
43
+ ActiveSupport::SecurityUtils.secure_compare(
44
+ Digest::SHA256.hexdigest(token),
45
+ Digest::SHA256.hexdigest(Motor::Configs::SYNC_ACCESS_KEY)
46
+ )
47
+
48
+ raise NotAuthenticated unless is_token_valid
49
+ end
50
+
51
+ def respond_with_configs
52
+ [
53
+ 200,
54
+ { 'Content-Type' => 'application/json' },
55
+ [Motor::Configs::BuildConfigsHash.call.to_json]
56
+ ]
57
+ rescue StandardError => e
58
+ [500, {}, [e.message]]
59
+ end
60
+
61
+ def sync_configs(body)
62
+ configs_hash = JSON.parse(body)
63
+
64
+ Motor::Configs::SyncFromHash.call(configs_hash)
65
+
66
+ [200, {}, []]
67
+ rescue StandardError => e
68
+ [500, {}, [e.message]]
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module SyncWithRemote
6
+ UnableToSync = Class.new(StandardError)
7
+ ApiNotFound = Class.new(StandardError)
8
+
9
+ module_function
10
+
11
+ def call(remote_url, api_key)
12
+ url = remote_url.delete_suffix('/') + Motor::Configs::SYNC_API_PATH
13
+
14
+ sync_from_remote!(url, api_key)
15
+ sync_to_remote!(url, api_key)
16
+ end
17
+
18
+ def sync_from_remote!(remote_url, api_key)
19
+ response = Motor::NetHttpUtils.get(remote_url, {}, { 'X-Authorization' => api_key })
20
+
21
+ raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
22
+ raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
23
+
24
+ configs_hash = JSON.parse(response.body)
25
+
26
+ Motor::Configs::SyncFromHash.call(configs_hash)
27
+ end
28
+
29
+ def sync_to_remote!(remote_url, api_key)
30
+ configs_hash = Motor::Configs::BuildConfigsHash.call
31
+
32
+ response = Motor::NetHttpUtils.post(
33
+ remote_url,
34
+ {},
35
+ {
36
+ 'X-Authorization' => api_key,
37
+ 'Content-Type' => 'application/json'
38
+ },
39
+ configs_hash.to_json
40
+ )
41
+
42
+ raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
43
+ raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module WriteToFile
6
+ THREAD_POOL = Concurrent::FixedThreadPool.new(1)
7
+
8
+ module_function
9
+
10
+ def call
11
+ return if Rails.env.test?
12
+ return if THREAD_POOL.queue_length.positive?
13
+
14
+ THREAD_POOL.post do
15
+ ActiveRecord::Base.logger.silence do
16
+ write_with_lock
17
+ end
18
+ rescue StandardError => e
19
+ Rails.logger.error(e)
20
+ end
21
+ end
22
+
23
+ def write_with_lock
24
+ File.open(Motor::Configs.file_path, 'w') do |file|
25
+ file.flock(File::LOCK_EX)
26
+
27
+ YAML.dump(Motor::Configs::BuildConfigsHash.call, file)
28
+
29
+ file.flock(File::LOCK_UN)
30
+
31
+ file
32
+ end.close
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ FILE_PATH = 'config/motor.yml'
6
+ SYNC_API_PATH = '/motor_configs_sync'
7
+ SYNC_ACCESS_KEY = ENV.fetch('MOTOR_SYNC_API_KEY', '')
8
+ WORKDIR_FILE_NAME = 'motor-admin.yml'
9
+
10
+ module_function
11
+
12
+ def clear
13
+ Motor::Resource.destroy_all
14
+ Motor::Alert.destroy_all
15
+ Motor::Query.destroy_all
16
+ Motor::Dashboard.destroy_all
17
+ Motor::Form.destroy_all
18
+ Motor::ApiConfig.destroy_all
19
+ Motor::Config.destroy_all
20
+ end
21
+
22
+ # @return [String]
23
+ def file_path
24
+ if defined?(MotorAdmin::Application)
25
+ [ENV['WORKDIR'], WORKDIR_FILE_NAME].compact.join('/')
26
+ else
27
+ Rails.root.join(FILE_PATH).to_s
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ require_relative 'configs/load_from_cache'
34
+ require_relative 'configs/build_ui_app_tag'
35
+ require_relative 'configs/build_configs_hash'
36
+ require_relative 'configs/write_to_file'
37
+ require_relative 'configs/sync_from_hash'
38
+ require_relative 'configs/sync_from_file'
39
+ require_relative 'configs/sync_with_remote'