motor-admin 0.1.35 → 0.1.41

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +70 -0
  3. data/app/controllers/motor/alerts_controller.rb +12 -6
  4. data/app/controllers/motor/audits_controller.rb +16 -0
  5. data/app/controllers/motor/configs_controller.rb +1 -0
  6. data/app/controllers/motor/dashboards_controller.rb +4 -0
  7. data/app/controllers/motor/data_controller.rb +2 -57
  8. data/app/controllers/motor/forms_controller.rb +4 -0
  9. data/app/controllers/motor/queries_controller.rb +4 -0
  10. data/app/controllers/motor/resources_controller.rb +1 -0
  11. data/app/controllers/motor/ui_controller.rb +4 -0
  12. data/app/models/motor/alert.rb +4 -2
  13. data/app/models/motor/application_record.rb +10 -0
  14. data/app/models/motor/audit.rb +9 -0
  15. data/app/models/motor/config.rb +2 -0
  16. data/app/models/motor/dashboard.rb +3 -1
  17. data/app/models/motor/form.rb +3 -1
  18. data/app/models/motor/query.rb +4 -1
  19. data/app/models/motor/resource.rb +2 -0
  20. data/app/views/motor/ui/show.html.erb +1 -1
  21. data/config/routes.rb +1 -0
  22. data/lib/generators/motor/templates/install.rb +40 -16
  23. data/lib/motor.rb +12 -3
  24. data/lib/motor/active_record_utils/defined_scopes_extension.rb +1 -1
  25. data/lib/motor/admin.rb +8 -0
  26. data/lib/motor/alerts/persistance.rb +17 -3
  27. data/lib/motor/alerts/scheduler.rb +1 -1
  28. data/lib/motor/api_query/apply_scope.rb +12 -5
  29. data/lib/motor/api_query/sort.rb +1 -1
  30. data/lib/motor/build_schema.rb +3 -3
  31. data/lib/motor/build_schema/find_display_column.rb +4 -4
  32. data/lib/motor/build_schema/load_from_rails.rb +20 -10
  33. data/lib/motor/build_schema/merge_schema_configs.rb +8 -4
  34. data/lib/motor/build_schema/reorder_schema.rb +10 -4
  35. data/lib/motor/configs.rb +17 -0
  36. data/lib/motor/configs/build_configs_hash.rb +83 -0
  37. data/lib/motor/configs/build_ui_app_tag.rb +71 -0
  38. data/lib/motor/configs/load_from_cache.rb +81 -0
  39. data/lib/motor/configs/sync_from_file.rb +36 -0
  40. data/lib/motor/configs/sync_from_hash.rb +126 -0
  41. data/lib/motor/configs/sync_middleware.rb +72 -0
  42. data/lib/motor/configs/sync_with_remote.rb +47 -0
  43. data/lib/motor/configs/write_to_file.rb +36 -0
  44. data/lib/motor/dashboards/persistance.rb +15 -5
  45. data/lib/motor/forms/persistance.rb +15 -5
  46. data/lib/motor/net_http_utils.rb +38 -0
  47. data/lib/motor/queries/persistance.rb +13 -3
  48. data/lib/motor/railtie.rb +11 -0
  49. data/lib/motor/tasks/motor.rake +37 -0
  50. data/lib/motor/version.rb +1 -1
  51. data/ui/dist/{main-03c3b1d3390877206e02.css.gz → main-0ef3be65da8d3b0dbabb.css.gz} +0 -0
  52. data/ui/dist/main-0ef3be65da8d3b0dbabb.js.gz +0 -0
  53. data/ui/dist/manifest.json +5 -5
  54. metadata +33 -5
  55. data/lib/motor/ui_configs.rb +0 -82
  56. data/ui/dist/main-03c3b1d3390877206e02.js.gz +0 -0
@@ -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.sub(%r{/\z}, '') + 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
+ FILE_PATH = Motor::Configs::FILE_PATH
8
+
9
+ module_function
10
+
11
+ def call
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(Rails.root.join(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
@@ -29,16 +29,20 @@ module Motor
29
29
  retry
30
30
  end
31
31
 
32
- def update_from_params!(dashboard, params)
32
+ def update_from_params!(dashboard, params, force_replace: false)
33
+ tag_ids = dashboard.tags.ids
34
+
33
35
  dashboard = assign_attributes(dashboard, params)
34
36
 
35
- raise TitleAlreadyExists if title_already_exists?(dashboard)
37
+ raise TitleAlreadyExists if !force_replace && title_already_exists?(dashboard)
36
38
 
37
39
  ApplicationRecord.transaction do
40
+ archive_with_existing_name(dashboard) if force_replace
41
+
38
42
  dashboard.save!
39
43
  end
40
44
 
41
- dashboard.tags.reload
45
+ dashboard.touch if tag_ids.sort != dashboard.tags.reload.ids.sort && params[:updated_at].blank?
42
46
 
43
47
  dashboard
44
48
  rescue ActiveRecord::RecordNotUnique
@@ -47,15 +51,21 @@ module Motor
47
51
 
48
52
  def assign_attributes(dashboard, params)
49
53
  dashboard.assign_attributes(params.slice(:title, :description, :preferences))
54
+ dashboard.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
50
55
 
51
56
  Motor::Tags.assign_tags(dashboard, params[:tags])
52
57
  end
53
58
 
59
+ def archive_with_existing_name(dashboard)
60
+ Motor::Dashboard.where(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
61
+ .update_all(deleted_at: Time.current)
62
+ end
63
+
54
64
  def title_already_exists?(dashboard)
55
65
  if dashboard.new_record?
56
- Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
66
+ Motor::Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
57
67
  else
58
- Dashboard.exists?(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
68
+ Motor::Dashboard.exists?(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
59
69
  end
60
70
  end
61
71
  end
@@ -29,16 +29,20 @@ module Motor
29
29
  retry
30
30
  end
31
31
 
32
- def update_from_params!(form, params)
32
+ def update_from_params!(form, params, force_replace: false)
33
+ tag_ids = form.tags.ids
34
+
33
35
  form = assign_attributes(form, params)
34
36
 
35
- raise NameAlreadyExists if name_already_exists?(form)
37
+ raise NameAlreadyExists if !force_replace && name_already_exists?(form)
36
38
 
37
39
  ApplicationRecord.transaction do
40
+ archive_with_existing_name(form) if force_replace
41
+
38
42
  form.save!
39
43
  end
40
44
 
41
- form.tags.reload
45
+ form.touch if tag_ids.sort != form.tags.reload.ids.sort && params[:updated_at].blank?
42
46
 
43
47
  form
44
48
  rescue ActiveRecord::RecordNotUnique
@@ -47,15 +51,21 @@ module Motor
47
51
 
48
52
  def assign_attributes(form, params)
49
53
  form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences))
54
+ form.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
50
55
 
51
56
  Motor::Tags.assign_tags(form, params[:tags])
52
57
  end
53
58
 
59
+ def archive_with_existing_name(form)
60
+ Motor::Form.where(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
61
+ .update_all(deleted_at: Time.current)
62
+ end
63
+
54
64
  def name_already_exists?(form)
55
65
  if form.new_record?
56
- Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
66
+ Motor::Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
57
67
  else
58
- Form.exists?(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
68
+ Motor::Form.exists?(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
59
69
  end
60
70
  end
61
71
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module NetHttpUtils
5
+ module_function
6
+
7
+ def get(url, params = {}, headers = {})
8
+ request = build_request(Net::HTTP::Get, url, params, headers, nil)
9
+
10
+ execute_request(request)
11
+ end
12
+
13
+ def post(url, params = {}, headers = {}, body = '')
14
+ request = build_request(Net::HTTP::Post, url, params, headers, body)
15
+
16
+ execute_request(request)
17
+ end
18
+
19
+ def build_request(method_class, url, params, headers, body)
20
+ uri = URI(url)
21
+ uri.query = params.to_query
22
+
23
+ request = method_class.new(uri)
24
+ request.body = body if body.present?
25
+ headers.each { |key, value| request[key] = value }
26
+
27
+ request
28
+ end
29
+
30
+ def execute_request(request)
31
+ uri = request.uri
32
+
33
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.port == 443) do |http|
34
+ http.request(request)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -29,16 +29,20 @@ module Motor
29
29
  retry
30
30
  end
31
31
 
32
- def update_from_params!(query, params)
32
+ def update_from_params!(query, params, force_replace: false)
33
+ tag_ids = query.tags.ids
34
+
33
35
  query = assign_attributes(query, params)
34
36
 
35
- raise NameAlreadyExists if name_already_exists?(query)
37
+ raise NameAlreadyExists if !force_replace && name_already_exists?(query)
36
38
 
37
39
  ApplicationRecord.transaction do
40
+ archive_with_existing_name(query) if force_replace
41
+
38
42
  query.save!
39
43
  end
40
44
 
41
- query.tags.reload
45
+ query.touch if tag_ids.sort != query.tags.reload.ids.sort && params[:updated_at].blank?
42
46
 
43
47
  query
44
48
  rescue ActiveRecord::RecordNotUnique
@@ -47,10 +51,16 @@ module Motor
47
51
 
48
52
  def assign_attributes(query, params)
49
53
  query.assign_attributes(params.slice(:name, :description, :sql_body, :preferences))
54
+ query.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
50
55
 
51
56
  Motor::Tags.assign_tags(query, params[:tags])
52
57
  end
53
58
 
59
+ def archive_with_existing_name(query)
60
+ Motor::Query.where(['lower(name) = ? AND id != ?', query.name.to_s.downcase, query.id])
61
+ .update_all(deleted_at: Time.current)
62
+ end
63
+
54
64
  def name_already_exists?(query)
55
65
  if query.new_record?
56
66
  Query.exists?(['lower(name) = ?', query.name.to_s.downcase])
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :motor_admin
6
+
7
+ rake_tasks do
8
+ Dir[Motor::PATH.join('./motor/tasks/**/*.rake')].each { |f| load f }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :motor do
4
+ desc 'Update configs/motor.yml file'
5
+
6
+ task dump: :environment do
7
+ Motor::Configs::WriteToFile.write_with_lock
8
+
9
+ puts '✅ configs/motor.yml has been updated'
10
+ end
11
+
12
+ desc 'Load configs from configs/motor.yml file'
13
+
14
+ task load: :environment do
15
+ Motor::Configs::SyncFromFile.call(with_exception: true)
16
+
17
+ puts '✅ configs have been loaded from configs/motor.yml'
18
+ end
19
+
20
+ desc 'Synchronize configs with remote application'
21
+
22
+ task sync: :environment do
23
+ remote_url = ENV['MOTOR_SYNC_REMOTE_URL']
24
+ api_key = ENV['MOTOR_SYNC_API_KEY']
25
+
26
+ raise 'Specify target app url using `MOTOR_SYNC_REMOTE_URL` env variable' if remote_url.blank?
27
+ raise 'Specify sync api key using `MOTOR_SYNC_API_KEY` env variable' if api_key.blank?
28
+
29
+ Motor::Configs::SyncWithRemote.call(remote_url, api_key)
30
+ Motor::Configs::WriteToFile.write_with_lock
31
+
32
+ puts "✅ Motor Admin configurations have been synced with #{remote_url}"
33
+ rescue Motor::Configs::SyncWithRemote::ApiNotFound
34
+ puts '⚠️ Synchronization failed: you need to specify `MOTOR_SYNC_API_KEY` ' \
35
+ 'env variable in your remote app in order to enable this feature'
36
+ end
37
+ end
data/lib/motor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- VERSION = '0.1.35'
4
+ VERSION = '0.1.41'
5
5
  end
@@ -5,9 +5,9 @@
5
5
  "fonts/ionicons.ttf?v=3.0.0-alpha.3": "fonts/ionicons.ttf",
6
6
  "fonts/ionicons.woff2?v=3.0.0-alpha.3": "fonts/ionicons.woff2",
7
7
  "fonts/ionicons.woff?v=3.0.0-alpha.3": "fonts/ionicons.woff",
8
- "main-03c3b1d3390877206e02.css.gz": "main-03c3b1d3390877206e02.css.gz",
9
- "main-03c3b1d3390877206e02.js.LICENSE.txt": "main-03c3b1d3390877206e02.js.LICENSE.txt",
10
- "main-03c3b1d3390877206e02.js.gz": "main-03c3b1d3390877206e02.js.gz",
11
- "main.css": "main-03c3b1d3390877206e02.css",
12
- "main.js": "main-03c3b1d3390877206e02.js"
8
+ "main-0ef3be65da8d3b0dbabb.css.gz": "main-0ef3be65da8d3b0dbabb.css.gz",
9
+ "main-0ef3be65da8d3b0dbabb.js.LICENSE.txt": "main-0ef3be65da8d3b0dbabb.js.LICENSE.txt",
10
+ "main-0ef3be65da8d3b0dbabb.js.gz": "main-0ef3be65da8d3b0dbabb.js.gz",
11
+ "main.css": "main-0ef3be65da8d3b0dbabb.css",
12
+ "main.js": "main-0ef3be65da8d3b0dbabb.js"
13
13
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motor-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.35
4
+ version: 0.1.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pete Matsyburka
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-11 00:00:00.000000000 Z
11
+ date: 2021-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: audited
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.9'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: cancancan
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -132,12 +146,14 @@ files:
132
146
  - LICENSE
133
147
  - README.md
134
148
  - Rakefile
149
+ - app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb
135
150
  - app/controllers/concerns/motor/wrap_io_params.rb
136
151
  - app/controllers/motor/active_storage_attachments_controller.rb
137
152
  - app/controllers/motor/alerts_controller.rb
138
153
  - app/controllers/motor/api_base_controller.rb
139
154
  - app/controllers/motor/application_controller.rb
140
155
  - app/controllers/motor/assets_controller.rb
156
+ - app/controllers/motor/audits_controller.rb
141
157
  - app/controllers/motor/configs_controller.rb
142
158
  - app/controllers/motor/dashboards_controller.rb
143
159
  - app/controllers/motor/data_controller.rb
@@ -157,6 +173,7 @@ files:
157
173
  - app/models/motor/alert.rb
158
174
  - app/models/motor/alert_lock.rb
159
175
  - app/models/motor/application_record.rb
176
+ - app/models/motor/audit.rb
160
177
  - app/models/motor/config.rb
161
178
  - app/models/motor/dashboard.rb
162
179
  - app/models/motor/form.rb
@@ -200,22 +217,33 @@ files:
200
217
  - lib/motor/build_schema/persist_resource_configs.rb
201
218
  - lib/motor/build_schema/reorder_schema.rb
202
219
  - lib/motor/build_schema/utils.rb
220
+ - lib/motor/configs.rb
221
+ - lib/motor/configs/build_configs_hash.rb
222
+ - lib/motor/configs/build_ui_app_tag.rb
223
+ - lib/motor/configs/load_from_cache.rb
224
+ - lib/motor/configs/sync_from_file.rb
225
+ - lib/motor/configs/sync_from_hash.rb
226
+ - lib/motor/configs/sync_middleware.rb
227
+ - lib/motor/configs/sync_with_remote.rb
228
+ - lib/motor/configs/write_to_file.rb
203
229
  - lib/motor/dashboards.rb
204
230
  - lib/motor/dashboards/persistance.rb
205
231
  - lib/motor/forms.rb
206
232
  - lib/motor/forms/persistance.rb
207
233
  - lib/motor/hash_serializer.rb
234
+ - lib/motor/net_http_utils.rb
208
235
  - lib/motor/queries.rb
209
236
  - lib/motor/queries/persistance.rb
210
237
  - lib/motor/queries/postgresql_exec_query.rb
211
238
  - lib/motor/queries/render_sql_template.rb
212
239
  - lib/motor/queries/run_query.rb
240
+ - lib/motor/railtie.rb
213
241
  - lib/motor/tags.rb
214
- - lib/motor/ui_configs.rb
242
+ - lib/motor/tasks/motor.rake
215
243
  - lib/motor/version.rb
216
244
  - ui/dist/fonts/ionicons.woff2
217
- - ui/dist/main-03c3b1d3390877206e02.css.gz
218
- - ui/dist/main-03c3b1d3390877206e02.js.gz
245
+ - ui/dist/main-0ef3be65da8d3b0dbabb.css.gz
246
+ - ui/dist/main-0ef3be65da8d3b0dbabb.js.gz
219
247
  - ui/dist/manifest.json
220
248
  homepage:
221
249
  licenses:
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Motor
4
- module UiConfigs
5
- CACHE_STORE =
6
- if Motor.development?
7
- ActiveSupport::Cache::NullStore.new
8
- else
9
- ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
10
- end
11
-
12
- module_function
13
-
14
- # @return [String]
15
- def app_tag
16
- CACHE_STORE.fetch(cache_key) do
17
- CACHE_STORE.clear
18
-
19
- Motor::ApplicationController.helpers.content_tag(
20
- :div, '', id: 'app', data: ui_data
21
- )
22
- end
23
- end
24
-
25
- # @return [Hash]
26
- def ui_data
27
- {
28
- base_path: Motor::Admin.routes.url_helpers.motor_path,
29
- schema: Motor::BuildSchema.call,
30
- header_links: header_links_data_hash,
31
- queries: queries_data_hash,
32
- dashboards: dashboards_data_hash,
33
- alerts: alerts_data_hash,
34
- forms: forms_data_hash
35
- }
36
- end
37
-
38
- def header_links_data_hash
39
- Motor::Config.find_by(key: 'header.links')&.value || []
40
- end
41
-
42
- def queries_data_hash
43
- Motor::Query.all.active.preload(:tags)
44
- .as_json(only: %i[id name updated_at],
45
- include: { tags: { only: %i[id name] } })
46
- end
47
-
48
- def dashboards_data_hash
49
- Motor::Dashboard.all.active.preload(:tags)
50
- .as_json(only: %i[id title updated_at],
51
- include: { tags: { only: %i[id name] } })
52
- end
53
-
54
- def alerts_data_hash
55
- Motor::Alert.all.active.preload(:tags)
56
- .as_json(only: %i[id name is_enabled updated_at],
57
- include: { tags: { only: %i[id name] } })
58
- end
59
-
60
- def forms_data_hash
61
- Motor::Form.all.active.preload(:tags)
62
- .as_json(only: %i[id name updated_at],
63
- include: { tags: { only: %i[id name] } })
64
- end
65
-
66
- # @return [String]
67
- def cache_key
68
- ActiveRecord::Base.connection.execute(
69
- "(#{
70
- [
71
- Motor::Config.select('MAX(updated_at)').to_sql,
72
- Motor::Resource.select('MAX(updated_at)').to_sql,
73
- Motor::Dashboard.select('MAX(updated_at)').to_sql,
74
- Motor::Alert.select('MAX(updated_at)').to_sql,
75
- Motor::Query.select('MAX(updated_at)').to_sql,
76
- Motor::Form.select('MAX(updated_at)').to_sql
77
- ].join(') UNION (')
78
- })"
79
- ).to_a.hash.to_s
80
- end
81
- end
82
- end