motor-admin 0.1.39 → 0.1.44

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/motor/active_storage_attachments_controller.rb +1 -1
  3. data/app/models/motor/alert.rb +2 -2
  4. data/app/models/motor/dashboard.rb +1 -1
  5. data/app/models/motor/form.rb +1 -1
  6. data/app/models/motor/query.rb +2 -1
  7. data/config/routes.rb +0 -2
  8. data/lib/generators/motor/templates/install.rb +10 -10
  9. data/lib/motor.rb +1 -1
  10. data/lib/motor/active_record_utils.rb +1 -0
  11. data/lib/motor/active_record_utils/active_storage_blob_patch.rb +25 -0
  12. data/lib/motor/active_record_utils/defined_scopes_extension.rb +1 -1
  13. data/lib/motor/admin.rb +12 -6
  14. data/lib/motor/alerts/persistance.rb +14 -3
  15. data/lib/motor/api_query/apply_scope.rb +12 -5
  16. data/lib/motor/build_schema.rb +1 -0
  17. data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
  18. data/lib/motor/build_schema/find_display_column.rb +32 -29
  19. data/lib/motor/build_schema/load_from_rails.rb +61 -31
  20. data/lib/motor/configs.rb +1 -0
  21. data/lib/motor/configs/build_configs_hash.rb +2 -2
  22. data/lib/motor/configs/sync_from_file.rb +3 -1
  23. data/lib/motor/configs/sync_from_hash.rb +4 -2
  24. data/lib/motor/configs/sync_middleware.rb +9 -9
  25. data/lib/motor/configs/sync_with_remote.rb +10 -2
  26. data/lib/motor/dashboards/persistance.rb +11 -4
  27. data/lib/motor/forms/persistance.rb +11 -4
  28. data/lib/motor/net_http_utils.rb +6 -5
  29. data/lib/motor/queries/persistance.rb +9 -2
  30. data/lib/motor/tasks/motor.rake +8 -0
  31. data/lib/motor/version.rb +1 -1
  32. data/ui/dist/{main-e443f49b386b119ff25a.css.gz → main-e763d59007bb725ea622.css.gz} +0 -0
  33. data/ui/dist/main-e763d59007bb725ea622.js.gz +0 -0
  34. data/ui/dist/manifest.json +5 -5
  35. metadata +6 -4
  36. data/ui/dist/main-e443f49b386b119ff25a.js.gz +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e0ab742c32ae1b59bfa12c6eb1f838e5d379cb5a964521fe7f345de5fabf953
4
- data.tar.gz: 9f018c3475ae06b4104e35f30774c80b2098a8a23b7fa3a8e4ac35df089ee98e
3
+ metadata.gz: 74c91b2c5dbc0b48fa75a4fa9f5ec2eb8a3848b2e65cf819b9ddc3317e379a19
4
+ data.tar.gz: bee5733d821cde96004674d36c9be0467e32044d921a4b52e863b4be4e3de55b
5
5
  SHA512:
6
- metadata.gz: '0651978240824b17dc1f379047faf9f5c5a393b9bebe6e37ae692a606630a300454a2c56c88173383defef451787555006d355fe26c2dd38173f0ece03e116a0'
7
- data.tar.gz: b15c006847a5c9dcea315741ccc3a46e3a2bba49ab05d2aceab44c75a530ba0f5fad79067aae0b8e31abad9cda105a827cbdb892d93fd1c2c1fe72a2f62a8637
6
+ metadata.gz: 9bb22f83f9c270696f2d32cc384b98d90a5dab99f7f377258682dbc3622bd574ea0c6bf43de5cd955bb551dd56d6d43b4e618a53803a7d91417a7ec88ae9571f
7
+ data.tar.gz: 6e1f295abe63ca50e7176789523747810384307c03bcf5c4c719cc41a85296573fb0f6c3e193f06e625ed5801c204a519675539719d966357398293fe3a10977
@@ -26,7 +26,7 @@ module Motor
26
26
  end
27
27
 
28
28
  def file_params
29
- params.require(:data).require(:file).permit(:io, :filename).to_h
29
+ params.require(:data).require(:file).permit(:io, :filename).to_h.symbolize_keys
30
30
  end
31
31
 
32
32
  def attachment_params
@@ -7,8 +7,8 @@ module Motor
7
7
  belongs_to :query
8
8
  belongs_to :author, polymorphic: true, optional: true
9
9
 
10
- has_many :alert_locks
11
- has_many :taggable_tags, as: :taggable
10
+ has_many :alert_locks, dependent: :destroy
11
+ has_many :taggable_tags, as: :taggable, dependent: :destroy
12
12
  has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
13
13
 
14
14
  serialize :preferences, HashSerializer
@@ -6,7 +6,7 @@ module Motor
6
6
 
7
7
  belongs_to :author, polymorphic: true, optional: true
8
8
 
9
- has_many :taggable_tags, as: :taggable
9
+ has_many :taggable_tags, as: :taggable, dependent: :destroy
10
10
  has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
11
11
 
12
12
  serialize :preferences, HashSerializer
@@ -6,7 +6,7 @@ module Motor
6
6
 
7
7
  belongs_to :author, polymorphic: true, optional: true
8
8
 
9
- has_many :taggable_tags, as: :taggable
9
+ has_many :taggable_tags, as: :taggable, dependent: :destroy
10
10
  has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
11
11
 
12
12
  serialize :preferences, HashSerializer
@@ -6,8 +6,9 @@ module Motor
6
6
 
7
7
  belongs_to :author, polymorphic: true, optional: true
8
8
 
9
- has_many :taggable_tags, as: :taggable
9
+ has_many :taggable_tags, as: :taggable, dependent: :destroy
10
10
  has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
11
+ has_many :alerts, dependent: :destroy
11
12
 
12
13
  serialize :preferences, HashSerializer
13
14
 
data/config/routes.rb CHANGED
@@ -47,5 +47,3 @@ Motor::Admin.routes.draw do
47
47
  end
48
48
  end
49
49
  end
50
-
51
- ActiveSupport::Notifications.instrument('motor.routes.loaded')
@@ -1,6 +1,6 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def self.up
3
- create_table :motor_queries, force: true do |t|
3
+ create_table :motor_queries do |t|
4
4
  t.column :name, :string, null: false
5
5
  t.column :description, :string
6
6
  t.column :sql_body, :string, null: false
@@ -18,7 +18,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
18
18
  where: 'deleted_at IS NULL'
19
19
  end
20
20
 
21
- create_table :motor_dashboards, force: true do |t|
21
+ create_table :motor_dashboards do |t|
22
22
  t.column :title, :string, null: false
23
23
  t.column :description, :string
24
24
  t.column :preferences, :string, null: false, default: '{}'
@@ -35,7 +35,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
35
35
  where: 'deleted_at IS NULL'
36
36
  end
37
37
 
38
- create_table :motor_forms, force: true do |t|
38
+ create_table :motor_forms do |t|
39
39
  t.column :name, :string, null: false
40
40
  t.column :description, :string
41
41
  t.column :api_path, :string, null: false
@@ -54,7 +54,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
54
54
  where: 'deleted_at IS NULL'
55
55
  end
56
56
 
57
- create_table :motor_resources, force: true do |t|
57
+ create_table :motor_resources do |t|
58
58
  t.column :name, :string, null: false, index: { unique: true }
59
59
  t.column :preferences, :string, null: false, default: '{}'
60
60
 
@@ -63,7 +63,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
63
63
  t.index :updated_at
64
64
  end
65
65
 
66
- create_table :motor_configs, force: true do |t|
66
+ create_table :motor_configs do |t|
67
67
  t.column :key, :string, null: false, index: { unique: true }
68
68
  t.column :value, :string, null: false, default: '{}'
69
69
 
@@ -72,7 +72,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
72
72
  t.index :updated_at
73
73
  end
74
74
 
75
- create_table :motor_alerts, force: true do |t|
75
+ create_table :motor_alerts do |t|
76
76
  t.references :query, null: false, foreign_key: { to_table: :motor_queries }, index: true
77
77
  t.column :name, :string, null: false
78
78
  t.column :description, :string
@@ -92,7 +92,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
92
92
  where: 'deleted_at IS NULL'
93
93
  end
94
94
 
95
- create_table :motor_alert_locks, force: true do |t|
95
+ create_table :motor_alert_locks do |t|
96
96
  t.references :alert, null: false, foreign_key: { to_table: :motor_alerts }
97
97
  t.column :lock_timestamp, :string, null: false
98
98
 
@@ -101,7 +101,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
101
101
  t.index %i[alert_id lock_timestamp], unique: true
102
102
  end
103
103
 
104
- create_table :motor_tags, force: true do |t|
104
+ create_table :motor_tags do |t|
105
105
  t.column :name, :string, null: false
106
106
 
107
107
  t.timestamps
@@ -111,7 +111,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
111
111
  unique: true
112
112
  end
113
113
 
114
- create_table :motor_taggable_tags, force: true do |t|
114
+ create_table :motor_taggable_tags do |t|
115
115
  t.references :tag, null: false, foreign_key: { to_table: :motor_tags }, index: true
116
116
  t.column :taggable_id, :integer, null: false
117
117
  t.column :taggable_type, :string, null: false
@@ -121,7 +121,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
121
121
  unique: true
122
122
  end
123
123
 
124
- create_table :motor_audits, force: true do |t|
124
+ create_table :motor_audits do |t|
125
125
  t.column :auditable_id, :integer
126
126
  t.column :auditable_type, :string
127
127
  t.column :associated_id, :integer
data/lib/motor.rb CHANGED
@@ -53,6 +53,7 @@ end
53
53
  require 'motor/version'
54
54
  require 'motor/admin'
55
55
  require 'motor/assets'
56
+ require 'motor/active_record_utils'
56
57
  require 'motor/build_schema'
57
58
  require 'motor/api_query'
58
59
  require 'motor/tags'
@@ -62,6 +63,5 @@ require 'motor/dashboards'
62
63
  require 'motor/forms'
63
64
  require 'motor/alerts'
64
65
  require 'motor/hash_serializer'
65
- require 'motor/active_record_utils'
66
66
  require 'motor/net_http_utils'
67
67
  require 'motor/railtie'
@@ -7,3 +7,4 @@ require_relative './active_record_utils/types'
7
7
  require_relative './active_record_utils/fetch_methods'
8
8
  require_relative './active_record_utils/defined_scopes_extension'
9
9
  require_relative './active_record_utils/active_storage_links_extension'
10
+ require_relative './active_record_utils/active_storage_blob_patch'
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ActiveRecordUtils
5
+ module ActiveStorageBlobPatch
6
+ KEYWORD_ARGS = %i[io filename content_type metadata identify].freeze
7
+
8
+ def build_after_upload(hash)
9
+ super(**hash.with_indifferent_access.slice(*KEYWORD_ARGS).symbolize_keys)
10
+ end
11
+
12
+ def build_after_unfurling(hash)
13
+ super(**hash.with_indifferent_access.slice(*KEYWORD_ARGS).symbolize_keys)
14
+ end
15
+
16
+ def create_after_unfurling!(hash)
17
+ super(**hash.with_indifferent_access.slice(*KEYWORD_ARGS).symbolize_keys)
18
+ end
19
+
20
+ def create_and_upload!(hash)
21
+ super(**hash.with_indifferent_access.slice(*KEYWORD_ARGS).symbolize_keys)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -4,7 +4,7 @@ module Motor
4
4
  module ActiveRecordUtils
5
5
  module DefinedScopesExtension
6
6
  def scope(name, _body)
7
- (@__scopes__ ||= []) << name
7
+ (@__scopes__ ||= []) << name.to_sym
8
8
 
9
9
  super
10
10
  end
data/lib/motor/admin.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'motor/configs/sync_middleware'
4
-
5
3
  module Motor
6
4
  class Admin < ::Rails::Engine
7
5
  initializer 'motor.startup_message' do
8
- ActiveSupport::Notifications.subscribe('motor.routes.loaded') do
6
+ config.after_initialize do
9
7
  next unless Motor.server?
10
8
 
9
+ Rails.application.reload_routes!
10
+
11
11
  if Rails.application.routes.url_helpers.respond_to?(:motor_admin_path)
12
12
  url =
13
13
  begin
@@ -31,9 +31,11 @@ module Motor
31
31
  end
32
32
 
33
33
  initializer 'motor.configs.sync_middleware' do
34
- if Motor::Configs::SyncMiddleware::ACCESS_KEY.present?
35
- Rails.application.config.middleware.insert_before(0, Motor::Configs::SyncMiddleware)
36
- end
34
+ next if Motor::Configs::SYNC_ACCESS_KEY.blank?
35
+
36
+ require 'motor/configs/sync_middleware'
37
+
38
+ Rails.application.config.middleware.insert_after(Rails::Rack::Logger, Motor::Configs::SyncMiddleware)
37
39
  end
38
40
 
39
41
  initializer 'motor.filter_params' do
@@ -67,6 +69,10 @@ module Motor
67
69
  ActiveSupport.on_load(:active_storage_attachment) do
68
70
  ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
69
71
  end
72
+
73
+ ActiveSupport.on_load(:active_storage_blob) do
74
+ ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
75
+ end
70
76
  end
71
77
  end
72
78
  end
@@ -43,25 +43,31 @@ module Motor
43
43
  retry
44
44
  end
45
45
 
46
- def update_from_params!(alert, params)
46
+ def update_from_params!(alert, params, force_replace: false)
47
47
  tag_ids = alert.tags.ids
48
48
 
49
49
  alert = assign_attributes(alert, params)
50
50
 
51
- raise NameAlreadyExists if name_already_exists?(alert)
51
+ raise NameAlreadyExists if !force_replace && name_already_exists?(alert)
52
52
  raise InvalidInterval unless alert.cron
53
53
 
54
54
  ApplicationRecord.transaction do
55
+ archive_with_existing_name(alert) if force_replace
56
+
55
57
  alert.save!
56
58
  end
57
59
 
58
- alert.touch if tag_ids.sort != alert.tags.reload.ids.sort && params[:updated_at].blank?
60
+ alert.touch if tags_changed?(tag_ids, alert) && params[:updated_at].blank?
59
61
 
60
62
  alert
61
63
  rescue ActiveRecord::RecordNotUnique
62
64
  retry
63
65
  end
64
66
 
67
+ def tags_changed?(previous_ids, alert)
68
+ previous_ids.sort != alert.tags.reload.ids.sort
69
+ end
70
+
65
71
  def assign_attributes(alert, params)
66
72
  alert.assign_attributes(params.slice(*ALERT_ATTRIBUTES))
67
73
  alert.preferences[:interval] = normalize_interval(alert.preferences[:interval])
@@ -70,6 +76,11 @@ module Motor
70
76
  Motor::Tags.assign_tags(alert, params[:tags])
71
77
  end
72
78
 
79
+ def archive_with_existing_name(alert)
80
+ Motor::Alert.where(['lower(name) = ? AND id != ?', alert.name.to_s.downcase, alert.id])
81
+ .update_all(deleted_at: Time.current)
82
+ end
83
+
73
84
  def normalize_interval(interval)
74
85
  interval.to_s.gsub(NORMALIZE_INTERVAL_REGEXP, 'every ')
75
86
  end
@@ -13,13 +13,20 @@ module Motor
13
13
  if rel.klass.defined_scopes.include?(scope_symbol)
14
14
  rel.public_send(scope_symbol)
15
15
  else
16
- configs = Motor::Resource.find_by_name(rel.klass.name.underscore)
17
- scope_configs = configs.preferences[:scopes].find { |s| s[:name] == scope }
16
+ apply_filter_scope(rel, scope)
17
+ end
18
+ end
18
19
 
19
- return rel unless scope_configs
20
+ def apply_filter_scope(rel, scope)
21
+ configs = Motor::Resource.find_by_name(rel.klass.name.underscore)
20
22
 
21
- ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
22
- end
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
+
29
+ ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
23
30
  end
24
31
  end
25
32
  end
@@ -68,6 +68,7 @@ module Motor
68
68
  end
69
69
 
70
70
  require_relative './build_schema/active_storage_attachment_schema'
71
+ require_relative './build_schema/adjust_devise_model_schema'
71
72
  require_relative './build_schema/load_from_rails'
72
73
  require_relative './build_schema/find_display_column'
73
74
  require_relative './build_schema/persist_resource_configs'
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module AdjustDeviseModelSchema
6
+ HIDDEN_COLUMNS = %w[
7
+ encrypted_password
8
+ reset_password_token
9
+ confirmation_token
10
+ ].freeze
11
+
12
+ READ_ONLY_COLUMNS = %w[
13
+ reset_password_sent_at
14
+ remember_created_at
15
+ sign_in_count
16
+ current_sign_in_at
17
+ last_sign_in_at
18
+ current_sign_in_ip
19
+ last_sign_in_ip
20
+ confirmed_at
21
+ confirmation_sent_at
22
+ ].freeze
23
+
24
+ module_function
25
+
26
+ def call(schema, devise_modules)
27
+ modify_column_access_types!(schema[:columns])
28
+ add_password_column!(schema[:columns]) if devise_modules.include?(:database_authenticatable)
29
+
30
+ schema
31
+ end
32
+
33
+ def modify_column_access_types!(columns)
34
+ columns.each do |column|
35
+ column[:access_type] =
36
+ case column[:name]
37
+ when *HIDDEN_COLUMNS
38
+ ColumnAccessTypes::HIDDEN
39
+ when *READ_ONLY_COLUMNS
40
+ ColumnAccessTypes::READ_ONLY
41
+ else
42
+ column[:access_type]
43
+ end
44
+ end
45
+ end
46
+
47
+ def add_password_column!(columns)
48
+ columns << {
49
+ name: 'password',
50
+ display_name: 'Password',
51
+ column_type: 'string',
52
+ access_type: 'write_only',
53
+ default_value: nil,
54
+ validators: [],
55
+ virtual: true
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -3,35 +3,38 @@
3
3
  module Motor
4
4
  module BuildSchema
5
5
  module FindDisplayColumn
6
- DISPLAY_NAMES = %w[
7
- name
8
- full_name
9
- fullname
10
- last_name
11
- lastname
12
- first_name
13
- firstname
14
- fname
15
- lname
16
- sname
17
- phone
18
- phone_number
19
- email
20
- domain
21
- phone
22
- company
23
- filename
24
- file_name
25
- title
26
- url
27
- make
28
- brand
29
- manufacturer
30
- model
31
- address
32
- ].freeze
33
-
34
- DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES).source, Regexp::IGNORECASE)
6
+ DISPLAY_NAMES = Set.new(
7
+ %w[
8
+ name
9
+ full_name
10
+ fullname
11
+ last_name
12
+ lastname
13
+ first_name
14
+ firstname
15
+ fname
16
+ lname
17
+ sname
18
+ company
19
+ domain
20
+ website
21
+ title
22
+ phone
23
+ phone_number
24
+ email
25
+ phone
26
+ filename
27
+ file_name
28
+ url
29
+ make
30
+ brand
31
+ manufacturer
32
+ model
33
+ address
34
+ ]
35
+ ).freeze
36
+
37
+ DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES.to_a).source, Regexp::IGNORECASE)
35
38
 
36
39
  module_function
37
40
 
@@ -4,12 +4,21 @@ module Motor
4
4
  module BuildSchema
5
5
  module LoadFromRails
6
6
  MUTEX = Mutex.new
7
+ UNIFIED_TYPES = ActiveRecordUtils::Types::UNIFIED_TYPES
7
8
 
8
9
  module_function
9
10
 
10
11
  def call
11
12
  models.map do |model|
12
- build_model_schema(model)
13
+ Object.const_get(model.name)
14
+
15
+ schema = build_model_schema(model)
16
+
17
+ if model.respond_to?(:devise_modules)
18
+ Motor::BuildSchema::AdjustDeviseModelSchema.call(schema, model.devise_modules)
19
+ end
20
+
21
+ schema
13
22
  rescue StandardError, NotImplementedError => e
14
23
  Rails.logger.error(e) if model.name != 'Audited::Audit'
15
24
 
@@ -20,8 +29,7 @@ module Motor
20
29
  def models
21
30
  eager_load_models!
22
31
 
23
- models = load_descendants(ActiveRecord::Base).uniq
24
- models = models.reject(&:abstract_class)
32
+ models = ActiveRecord::Base.descendants.reject(&:abstract_class)
25
33
 
26
34
  models -= Motor::ApplicationRecord.descendants
27
35
  models -= [Motor::Audit]
@@ -32,12 +40,6 @@ module Motor
32
40
  models
33
41
  end
34
42
 
35
- def load_descendants(model)
36
- model.descendants + model.descendants.flat_map do |klass|
37
- load_descendants(klass)
38
- end
39
- end
40
-
41
43
  def build_model_schema(model)
42
44
  model_name = model.name
43
45
 
@@ -92,10 +94,13 @@ module Motor
92
94
  end
93
95
 
94
96
  def build_table_column(column, model, default_attrs)
97
+ is_enum = model.defined_enums[column.name]
98
+
95
99
  {
96
100
  name: column.name,
97
101
  display_name: column.name.humanize,
98
- column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
102
+ column_type: is_enum ? 'string' : UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
103
+ is_array: column.array?,
99
104
  access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
100
105
  default_value: default_attrs[column.name],
101
106
  validators: fetch_validators(model, column.name),
@@ -111,13 +116,15 @@ module Motor
111
116
  model.reflections.map do |name, ref|
112
117
  next if !ref.has_one? && !ref.belongs_to?
113
118
 
114
- begin
115
- ref.klass
116
- rescue StandardError
117
- next
118
- end
119
+ unless ref.polymorphic?
120
+ begin
121
+ next if ref.klass.name == 'ActiveStorage::Blob'
122
+ rescue StandardError => e
123
+ Rails.logger.error(e)
119
124
 
120
- next if ref.klass.name == 'ActiveStorage::Blob'
125
+ next
126
+ end
127
+ end
121
128
 
122
129
  build_reflection_column(name, model, ref, default_attrs)
123
130
  end.compact
@@ -125,7 +132,7 @@ module Motor
125
132
 
126
133
  def build_reflection_column(name, model, ref, default_attrs)
127
134
  column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
128
- is_attachment = ref.klass.name == 'ActiveStorage::Attachment'
135
+ is_attachment = !ref.polymorphic? && ref.klass.name == 'ActiveStorage::Attachment'
129
136
  access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
130
137
 
131
138
  {
@@ -136,26 +143,26 @@ module Motor
136
143
  default_value: default_attrs[column_name],
137
144
  validators: fetch_validators(model, column_name, ref),
138
145
  format: {},
139
- reference: {
140
- name: name,
141
- model_name: ref.klass.name.underscore,
142
- reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
143
- foreign_key: ref.foreign_key,
144
- polymorphic: ref.polymorphic? || is_attachment
145
- },
146
+ reference: build_reference(name, ref),
146
147
  virtual: false
147
148
  }
148
149
  end
149
150
 
151
+ def build_reference(name, reflection)
152
+ {
153
+ name: name,
154
+ display_name: name.humanize,
155
+ model_name: reflection.polymorphic? ? nil : reflection.klass.name.underscore,
156
+ reference_type: reflection.belongs_to? ? 'belongs_to' : 'has_one',
157
+ foreign_key: reflection.foreign_key,
158
+ polymorphic: reflection.polymorphic?
159
+ }
160
+ end
161
+
150
162
  def fetch_associations(model)
151
163
  model.reflections.map do |name, ref|
152
164
  next if ref.has_one? || ref.belongs_to?
153
-
154
- begin
155
- ref.klass
156
- rescue StandardError
157
- next
158
- end
165
+ next unless valid_reflection?(ref)
159
166
 
160
167
  model_class = ref.klass
161
168
 
@@ -167,7 +174,7 @@ module Motor
167
174
  slug: name.underscore,
168
175
  model_name: model_class.name.underscore,
169
176
  foreign_key: ref.foreign_key,
170
- polymorphic: ref.polymorphic? || model_class.name == 'ActiveStorage::Attachment',
177
+ polymorphic: ref.options[:as].present?,
171
178
  visible: true
172
179
  }
173
180
  end.compact
@@ -181,6 +188,10 @@ module Motor
181
188
  []
182
189
  end
183
190
 
191
+ enum = model.defined_enums[column_name]
192
+
193
+ validators += [{ includes: enum.keys }] if enum
194
+
184
195
  validators += model.validators_on(column_name).map do |validator|
185
196
  build_validator_hash(validator)
186
197
  end.compact
@@ -203,6 +214,17 @@ module Motor
203
214
  end
204
215
  end
205
216
 
217
+ def valid_reflection?(reflection)
218
+ reflection.klass
219
+ reflection.foreign_key
220
+
221
+ true
222
+ rescue StandardError => e
223
+ Rails.logger.error(e)
224
+
225
+ false
226
+ end
227
+
206
228
  def eager_load_models!
207
229
  MUTEX.synchronize do
208
230
  if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
@@ -210,6 +232,14 @@ module Motor
210
232
  else
211
233
  Rails.application.eager_load!
212
234
  end
235
+
236
+ ActiveRecord::Base.descendants.each do |model|
237
+ model.reflections.each do |_, ref|
238
+ ref.klass
239
+ rescue StandardError
240
+ next
241
+ end
242
+ end
213
243
  end
214
244
  end
215
245
  end
data/lib/motor/configs.rb CHANGED
@@ -4,6 +4,7 @@ module Motor
4
4
  module Configs
5
5
  FILE_PATH = 'config/motor.yml'
6
6
  SYNC_API_PATH = '/motor_configs_sync'
7
+ SYNC_ACCESS_KEY = ENV.fetch('MOTOR_SYNC_API_KEY', '')
7
8
  end
8
9
  end
9
10
 
@@ -9,8 +9,8 @@ module Motor
9
9
  cache_keys = LoadFromCache.load_cache_keys
10
10
 
11
11
  normalize_hash(
12
- app_version: Motor::VERSION,
13
- file_version: cache_keys.values.max.to_time,
12
+ engine_version: Motor::VERSION,
13
+ file_version: cache_keys.values.compact.max.to_time,
14
14
  resources: build_resources_hash(cache_keys[:resources]),
15
15
  configs: build_configs_hash(cache_keys[:configs]),
16
16
  queries: build_queries_hash(cache_keys[:queries]),
@@ -9,7 +9,7 @@ module Motor
9
9
 
10
10
  module_function
11
11
 
12
- def call
12
+ def call(with_exception: false)
13
13
  MUTEXT.synchronize do
14
14
  file = Rails.root.join(FILE_PATH)
15
15
 
@@ -17,6 +17,8 @@ module Motor
17
17
  begin
18
18
  file.ctime
19
19
  rescue Errno::ENOENT
20
+ raise if with_exception
21
+
20
22
  nil
21
23
  end
22
24
 
@@ -6,6 +6,8 @@ module Motor
6
6
  module_function
7
7
 
8
8
  def call(configs_hash)
9
+ return if configs_hash.blank?
10
+
9
11
  configs_hash = configs_hash.with_indifferent_access
10
12
 
11
13
  Motor::ApplicationRecord.transaction do
@@ -100,7 +102,7 @@ module Motor
100
102
 
101
103
  next if record.updated_at >= attrs[:updated_at]
102
104
 
103
- update_proc.call(record, attrs)
105
+ update_proc.call(record, attrs, force_replace: true)
104
106
  end
105
107
  end
106
108
 
@@ -108,7 +110,7 @@ module Motor
108
110
  create_items.each do |attrs|
109
111
  record = records_class.find_or_initialize_by(id: attrs[:id]).tap { |e| e.deleted_at = nil }
110
112
 
111
- update_proc.call(record, attrs)
113
+ update_proc.call(record, attrs, force_replace: true)
112
114
  end
113
115
  end
114
116
 
@@ -3,8 +3,6 @@
3
3
  module Motor
4
4
  module Configs
5
5
  class SyncMiddleware
6
- ACCESS_KEY = ENV.fetch('MOTOR_SYNC_API_KEY', '')
7
-
8
6
  KeyNotSpecified = Class.new(StandardError)
9
7
  NotAuthenticated = Class.new(StandardError)
10
8
 
@@ -14,13 +12,15 @@ module Motor
14
12
 
15
13
  def call(env)
16
14
  if env['PATH_INFO'] == Motor::Configs::SYNC_API_PATH
17
- authenticate!(env['QUERY_STRING'])
15
+ authenticate!(env['HTTP_X_AUTHORIZATION'])
18
16
 
19
17
  case env['REQUEST_METHOD']
20
18
  when 'GET'
21
19
  respond_with_configs
22
20
  when 'POST'
23
- sync_configs(env['rack.input'].read)
21
+ input = env['rack.input']
22
+ input.rewind
23
+ sync_configs(input.read)
24
24
  else
25
25
  @app.call(env)
26
26
  end
@@ -35,14 +35,14 @@ module Motor
35
35
 
36
36
  private
37
37
 
38
- def authenticate!(query_string)
39
- raise KeyNotSpecified if ACCESS_KEY.blank?
40
- raise NotAuthenticated if query_string.blank?
38
+ def authenticate!(token)
39
+ raise KeyNotSpecified if Motor::Configs::SYNC_ACCESS_KEY.blank?
40
+ raise NotAuthenticated if token.blank?
41
41
 
42
42
  is_token_valid =
43
43
  ActiveSupport::SecurityUtils.secure_compare(
44
- Digest::SHA256.hexdigest(query_string),
45
- Digest::SHA256.hexdigest("token=#{ACCESS_KEY}")
44
+ Digest::SHA256.hexdigest(token),
45
+ Digest::SHA256.hexdigest(Motor::Configs::SYNC_ACCESS_KEY)
46
46
  )
47
47
 
48
48
  raise NotAuthenticated unless is_token_valid
@@ -16,7 +16,7 @@ module Motor
16
16
  end
17
17
 
18
18
  def sync_from_remote!(remote_url, api_key)
19
- response = Motor::NetHttpUtils.get(remote_url, { token: api_key })
19
+ response = Motor::NetHttpUtils.get(remote_url, {}, { 'X-Authorization' => api_key })
20
20
 
21
21
  raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
22
22
  raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
@@ -29,7 +29,15 @@ module Motor
29
29
  def sync_to_remote!(remote_url, api_key)
30
30
  configs_hash = Motor::Configs::BuildConfigsHash.call
31
31
 
32
- response = Motor::NetHttpUtils.post(remote_url, { token: api_key }, configs_hash.to_json)
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
+ )
33
41
 
34
42
  raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
35
43
  raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
@@ -29,14 +29,16 @@ 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
33
  tag_ids = dashboard.tags.ids
34
34
 
35
35
  dashboard = assign_attributes(dashboard, params)
36
36
 
37
- raise TitleAlreadyExists if title_already_exists?(dashboard)
37
+ raise TitleAlreadyExists if !force_replace && title_already_exists?(dashboard)
38
38
 
39
39
  ApplicationRecord.transaction do
40
+ archive_with_existing_name(dashboard) if force_replace
41
+
40
42
  dashboard.save!
41
43
  end
42
44
 
@@ -54,11 +56,16 @@ module Motor
54
56
  Motor::Tags.assign_tags(dashboard, params[:tags])
55
57
  end
56
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
+
57
64
  def title_already_exists?(dashboard)
58
65
  if dashboard.new_record?
59
- Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
66
+ Motor::Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
60
67
  else
61
- 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])
62
69
  end
63
70
  end
64
71
  end
@@ -29,14 +29,16 @@ 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
33
  tag_ids = form.tags.ids
34
34
 
35
35
  form = assign_attributes(form, params)
36
36
 
37
- raise NameAlreadyExists if name_already_exists?(form)
37
+ raise NameAlreadyExists if !force_replace && name_already_exists?(form)
38
38
 
39
39
  ApplicationRecord.transaction do
40
+ archive_with_existing_name(form) if force_replace
41
+
40
42
  form.save!
41
43
  end
42
44
 
@@ -54,11 +56,16 @@ module Motor
54
56
  Motor::Tags.assign_tags(form, params[:tags])
55
57
  end
56
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
+
57
64
  def name_already_exists?(form)
58
65
  if form.new_record?
59
- Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
66
+ Motor::Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
60
67
  else
61
- 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])
62
69
  end
63
70
  end
64
71
  end
@@ -4,24 +4,25 @@ module Motor
4
4
  module NetHttpUtils
5
5
  module_function
6
6
 
7
- def get(url, params = {})
8
- request = build_request(Net::HTTP::Get, url, params, nil)
7
+ def get(url, params = {}, headers = {})
8
+ request = build_request(Net::HTTP::Get, url, params, headers, nil)
9
9
 
10
10
  execute_request(request)
11
11
  end
12
12
 
13
- def post(url, params = {}, body = '')
14
- request = build_request(Net::HTTP::Post, url, params, body)
13
+ def post(url, params = {}, headers = {}, body = '')
14
+ request = build_request(Net::HTTP::Post, url, params, headers, body)
15
15
 
16
16
  execute_request(request)
17
17
  end
18
18
 
19
- def build_request(method_class, url, params, body)
19
+ def build_request(method_class, url, params, headers, body)
20
20
  uri = URI(url)
21
21
  uri.query = params.to_query
22
22
 
23
23
  request = method_class.new(uri)
24
24
  request.body = body if body.present?
25
+ headers.each { |key, value| request[key] = value }
25
26
 
26
27
  request
27
28
  end
@@ -29,14 +29,16 @@ 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
33
  tag_ids = query.tags.ids
34
34
 
35
35
  query = assign_attributes(query, params)
36
36
 
37
- raise NameAlreadyExists if name_already_exists?(query)
37
+ raise NameAlreadyExists if !force_replace && name_already_exists?(query)
38
38
 
39
39
  ApplicationRecord.transaction do
40
+ archive_with_existing_name(query) if force_replace
41
+
40
42
  query.save!
41
43
  end
42
44
 
@@ -54,6 +56,11 @@ module Motor
54
56
  Motor::Tags.assign_tags(query, params[:tags])
55
57
  end
56
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
+
57
64
  def name_already_exists?(query)
58
65
  if query.new_record?
59
66
  Query.exists?(['lower(name) = ?', query.name.to_s.downcase])
@@ -9,6 +9,14 @@ namespace :motor do
9
9
  puts '✅ configs/motor.yml has been updated'
10
10
  end
11
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
+
12
20
  desc 'Synchronize configs with remote application'
13
21
 
14
22
  task sync: :environment do
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.39'
4
+ VERSION = '0.1.44'
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-e443f49b386b119ff25a.css.gz": "main-e443f49b386b119ff25a.css.gz",
9
- "main-e443f49b386b119ff25a.js.LICENSE.txt": "main-e443f49b386b119ff25a.js.LICENSE.txt",
10
- "main-e443f49b386b119ff25a.js.gz": "main-e443f49b386b119ff25a.js.gz",
11
- "main.css": "main-e443f49b386b119ff25a.css",
12
- "main.js": "main-e443f49b386b119ff25a.js"
8
+ "main-e763d59007bb725ea622.css.gz": "main-e763d59007bb725ea622.css.gz",
9
+ "main-e763d59007bb725ea622.js.LICENSE.txt": "main-e763d59007bb725ea622.js.LICENSE.txt",
10
+ "main-e763d59007bb725ea622.js.gz": "main-e763d59007bb725ea622.js.gz",
11
+ "main.css": "main-e763d59007bb725ea622.css",
12
+ "main.js": "main-e763d59007bb725ea622.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.39
4
+ version: 0.1.44
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-15 00:00:00.000000000 Z
11
+ date: 2021-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -191,6 +191,7 @@ files:
191
191
  - lib/motor-admin.rb
192
192
  - lib/motor.rb
193
193
  - lib/motor/active_record_utils.rb
194
+ - lib/motor/active_record_utils/active_storage_blob_patch.rb
194
195
  - lib/motor/active_record_utils/active_storage_links_extension.rb
195
196
  - lib/motor/active_record_utils/defined_scopes_extension.rb
196
197
  - lib/motor/active_record_utils/fetch_methods.rb
@@ -211,6 +212,7 @@ files:
211
212
  - lib/motor/assets.rb
212
213
  - lib/motor/build_schema.rb
213
214
  - lib/motor/build_schema/active_storage_attachment_schema.rb
215
+ - lib/motor/build_schema/adjust_devise_model_schema.rb
214
216
  - lib/motor/build_schema/find_display_column.rb
215
217
  - lib/motor/build_schema/load_from_rails.rb
216
218
  - lib/motor/build_schema/merge_schema_configs.rb
@@ -242,8 +244,8 @@ files:
242
244
  - lib/motor/tasks/motor.rake
243
245
  - lib/motor/version.rb
244
246
  - ui/dist/fonts/ionicons.woff2
245
- - ui/dist/main-e443f49b386b119ff25a.css.gz
246
- - ui/dist/main-e443f49b386b119ff25a.js.gz
247
+ - ui/dist/main-e763d59007bb725ea622.css.gz
248
+ - ui/dist/main-e763d59007bb725ea622.js.gz
247
249
  - ui/dist/manifest.json
248
250
  homepage:
249
251
  licenses: