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,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'