motor-admin 0.2.37 → 0.2.43

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/motor/api_configs_controller.rb +54 -0
  3. data/app/controllers/motor/forms_controller.rb +2 -1
  4. data/app/controllers/motor/queries_controller.rb +5 -1
  5. data/app/controllers/motor/run_api_requests_controller.rb +49 -0
  6. data/app/models/motor/api_config.rb +29 -0
  7. data/app/models/motor/form.rb +1 -0
  8. data/config/locales/en.yml +6 -0
  9. data/config/locales/es.yml +6 -0
  10. data/config/locales/pt.yml +6 -0
  11. data/config/routes.rb +2 -0
  12. data/lib/generators/motor/templates/install.rb +21 -0
  13. data/lib/generators/motor/templates/install_api_configs.rb +80 -0
  14. data/lib/generators/motor/upgrade_generator.rb +28 -0
  15. data/lib/motor/active_record_utils/types.rb +1 -1
  16. data/lib/motor/alerts/persistance.rb +2 -2
  17. data/lib/motor/api_configs.rb +25 -0
  18. data/lib/motor/api_query/apply_scope.rb +11 -1
  19. data/lib/motor/build_schema/find_searchable_columns.rb +1 -1
  20. data/lib/motor/configs/build_configs_hash.rb +11 -2
  21. data/lib/motor/configs/load_from_cache.rb +8 -1
  22. data/lib/motor/configs/sync_from_hash.rb +23 -0
  23. data/lib/motor/dashboards/persistance.rb +2 -2
  24. data/lib/motor/forms/persistance.rb +23 -3
  25. data/lib/motor/net_http_utils.rb +14 -2
  26. data/lib/motor/queries/persistance.rb +19 -2
  27. data/lib/motor/queries/run_query.rb +3 -1
  28. data/lib/motor/resources/fetch_configured_model.rb +1 -1
  29. data/lib/motor/resources.rb +2 -1
  30. data/lib/motor/version.rb +1 -1
  31. data/lib/motor.rb +1 -0
  32. data/ui/dist/main-90db17316da397cbfa9c.css.gz +0 -0
  33. data/ui/dist/main-90db17316da397cbfa9c.js.gz +0 -0
  34. data/ui/dist/manifest.json +5 -5
  35. metadata +10 -4
  36. data/ui/dist/main-caf0e2eb1b4372864962.css.gz +0 -0
  37. data/ui/dist/main-caf0e2eb1b4372864962.js.gz +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a582148a923d7c89a0944ba2c91315b434db0989a6b8918ba4db1f36d8f17480
4
- data.tar.gz: ed1d8c3588dff89c78e468b6b4a9ac06def1a3af163dc74d6a6602321a0e6dc1
3
+ metadata.gz: 89d36e62222311f43cf9df24d7d090d72cb067e9cc713431ff2f7ce39ed8079c
4
+ data.tar.gz: 40f0520672effff61094e2518afcb216c6da4c2402749bac024648a5a8c491e9
5
5
  SHA512:
6
- metadata.gz: b3a9a370786068bf97186bf93ef42de50318a2bbbcf2ed52c83aeef3705693d90ecd4fa40e012cb55c56303576eb124a27cb19ac16db584b47138733cca1e223
7
- data.tar.gz: 9e03d29bb4d5e9ee67c4c2b7d4d809294caaf2e4bedd2b3b9c2cba61d65888edf2d7cd47ef5c28503bbcef916092e34979c3b4ee34be137cb8fb9306f9cc216a
6
+ metadata.gz: 89e962e2d87c0171a38961d1f1d0ede97021d5eb8bb0ba071b92d3c114a919d201f11dd25647793efd656823fc7b19c7e913c51a73b7e69b7f472f8772a59fa5
7
+ data.tar.gz: 38d3105d72a4aca9fa91f0096e92d7c4ef015e0544e877ceb7b844eac6617cbdc1c25a6df20772358b5a93b29b661985d6112719cfae9e62737499306b05c695
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApiConfigsController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
7
+ before_action :find_or_initialize_api_config, only: :create
8
+ load_and_authorize_resource
9
+
10
+ def index
11
+ render json: { data: Motor::ApiQuery::BuildJson.call(@api_configs.active.order(:id), params, current_ability) }
12
+ end
13
+
14
+ def create
15
+ @api_config.save!
16
+
17
+ Motor::Configs::WriteToFile.call
18
+
19
+ render json: { data: Motor::ApiQuery::BuildJson.call(@api_config, params, current_ability) }
20
+ rescue ActiveRecord::RecordNotUnique
21
+ find_or_initialize_api_config
22
+
23
+ retry
24
+ end
25
+
26
+ def destroy
27
+ @api_config&.update!(deleted_at: Time.current)
28
+
29
+ Motor::Configs::WriteToFile.call
30
+
31
+ head :ok
32
+ end
33
+
34
+ private
35
+
36
+ def find_or_initialize_api_config
37
+ name, url, description, preferences, credentials =
38
+ api_config_params.values_at(:name, :url, :description, :preferences, :credentials)
39
+
40
+ @api_config =
41
+ Motor::ApiConfig.find_or_initialize_by(name: name).tap do |config|
42
+ config.url = url.delete_suffix('/')
43
+ config.description = description
44
+ config.preferences.merge!(preferences)
45
+ config.credentials.merge!(credentials)
46
+ config.deleted_at = nil
47
+ end
48
+ end
49
+
50
+ def api_config_params
51
+ params.require(:data).permit(:name, :url, :description, preferences: {}, credentials: {})
52
+ end
53
+ end
54
+ end
@@ -54,7 +54,8 @@ module Motor
54
54
  end
55
55
 
56
56
  def form_params
57
- params.require(:data).permit(:name, :description, :api_path, :http_method, preferences: {}, tags: [])
57
+ params.require(:data).permit(:name, :description, :api_path, :http_method,
58
+ :api_config_name, preferences: {}, tags: [])
58
59
  end
59
60
  end
60
61
  end
@@ -21,7 +21,11 @@ module Motor
21
21
  if Motor::Queries::Persistance.name_already_exists?(@query)
22
22
  render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
23
23
  else
24
- ApplicationRecord.transaction { @query.save! }
24
+ ApplicationRecord.transaction do
25
+ Motor::Queries::Persistance.assign_or_create_api_config!(@query)
26
+ @query.save!
27
+ end
28
+
25
29
  Motor::Configs::WriteToFile.call
26
30
 
27
31
  render json: { data: Motor::ApiQuery::BuildJson.call(@query, params, current_ability) }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class RunApiRequestsController < ApiBaseController
5
+ JWT_TTL = 2.hours
6
+
7
+ wrap_parameters :data
8
+
9
+ def show
10
+ respond_with_result
11
+ end
12
+
13
+ def create
14
+ respond_with_result
15
+ end
16
+
17
+ private
18
+
19
+ def respond_with_result
20
+ response = Motor::ApiConfigs.run(find_or_initialize_api_config,
21
+ method: request_params[:method],
22
+ path: request_params[:path],
23
+ body: request_params[:params],
24
+ params: request_params[:body],
25
+ headers: { 'Authorization' => "Bearer #{current_user_jwt}" })
26
+
27
+ self.response_body = response.body
28
+ self.status = response.code.to_i
29
+ end
30
+
31
+ def find_or_initialize_api_config
32
+ Motor::ApiConfig.find_by(name: request_params[:api_config_name]) ||
33
+ Motor::ApiConfig.new(url: request_params[:api_config_name])
34
+ end
35
+
36
+ def current_user_jwt
37
+ return '' unless defined?(JWT)
38
+ return '' unless current_user
39
+
40
+ payload = { uid: current_user.id, email: current_user.email, exp: JWT_TTL.from_now.to_i }
41
+
42
+ JWT.encode(payload, Rails.application.secrets.secret_key_base)
43
+ end
44
+
45
+ def request_params
46
+ params.require(:data).permit!
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApiConfig < ::Motor::ApplicationRecord
5
+ unless table_exists?
6
+ puts
7
+ puts ' => Run `rails g motor:upgrade && rake db:migrate` to perform data migration and enable the latest features'
8
+ puts
9
+ end
10
+
11
+ encrypts :credentials if defined?(::Motor::EncryptedConfig)
12
+
13
+ serialize :credentials, Motor::HashSerializer
14
+ serialize :preferences, Motor::HashSerializer
15
+
16
+ attribute :preferences, default: -> { HashWithIndifferentAccess.new }
17
+ attribute :credentials, default: -> { HashWithIndifferentAccess.new }
18
+
19
+ has_one :form, dependent: nil, foreign_key: :api_config_name, primary_key: :name, inverse_of: :api_config
20
+
21
+ scope :active, -> { where(deleted_at: nil) }
22
+
23
+ def headers
24
+ credentials.fetch(:headers, []).each_with_object({}) do |item, acc|
25
+ acc[item[:key]] = item[:value]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -5,6 +5,7 @@ module Motor
5
5
  audited
6
6
 
7
7
  belongs_to :author, polymorphic: true, optional: true
8
+ belongs_to :api_config, foreign_key: :api_config_name, primary_key: :name, inverse_of: :form
8
9
 
9
10
  has_many :taggable_tags, as: :taggable, dependent: :destroy
10
11
  has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
@@ -293,3 +293,9 @@ en:
293
293
  regexp: RegExp
294
294
  message: Message
295
295
  should_be_a_valid_regexp: Should be a valid RegExp string
296
+ order_by: Order by
297
+ ascending: Ascending
298
+ descending: Descending
299
+ url: URL
300
+ expand: Expand
301
+ add_api: Add API
@@ -293,6 +293,12 @@ es:
293
293
  regexp: RegExp
294
294
  message: Mensaje
295
295
  should_be_a_valid_regexp: Debe ser una cadena RegExp válida
296
+ order_by: Ordenar por
297
+ ascending: Ascendente
298
+ descending: Descendente
299
+ url: URL
300
+ expand: Expandir
301
+ add_api: De API
296
302
  i:
297
303
  locale: es
298
304
  select:
@@ -289,6 +289,12 @@ pt:
289
289
  regexp: RegExp
290
290
  message: Mensagem
291
291
  should_be_a_valid_regexp: Deve ser uma string RegExp válida
292
+ order_by: Encomendar por
293
+ ascending: Ascendente
294
+ descending: Decrescente
295
+ url: URL
296
+ expand: Expandir
297
+ add_api: De API
292
298
  i:
293
299
  locale: pt
294
300
  select:
data/config/routes.rb CHANGED
@@ -14,6 +14,8 @@ Motor::Admin.routes.draw do
14
14
  resources :resource_default_queries, only: %i[show], param: 'resource'
15
15
  resources :schema, only: %i[index show], param: 'resource'
16
16
  resources :dashboards, only: %i[index show create update destroy]
17
+ resource :run_api_request, only: %i[show create]
18
+ resources :api_configs, only: %i[index create destroy]
17
19
  resources :forms, only: %i[index show create update destroy]
18
20
  resources :alerts, only: %i[index show create update destroy]
19
21
  resources :icons, only: %i[index]
@@ -44,6 +44,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
44
44
  t.column :author_id, :bigint
45
45
  t.column :author_type, :string
46
46
  t.column :deleted_at, :datetime
47
+ t.column :api_config_name, :string, null: false
47
48
 
48
49
  t.timestamps
49
50
 
@@ -138,6 +139,22 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
138
139
  t.column :created_at, :datetime
139
140
  end
140
141
 
142
+ create_table :motor_api_configs do |t|
143
+ t.column :name, :string, null: false
144
+ t.column :url, :string, null: false
145
+ t.column :preferences, :text, null: false
146
+ t.column :credentials, :text, null: false
147
+ t.column :description, :text
148
+ t.column :deleted_at, :datetime
149
+
150
+ t.timestamps
151
+
152
+ t.index 'name',
153
+ name: 'motor_api_configs_name_unique_index',
154
+ unique: true,
155
+ where: 'deleted_at IS NULL'
156
+ end
157
+
141
158
  add_index :motor_audits, %i[auditable_type auditable_id version], name: 'motor_auditable_index'
142
159
  add_index :motor_audits, %i[associated_type associated_id], name: 'motor_auditable_associated_index'
143
160
  add_index :motor_audits, %i[user_id user_type], name: 'motor_auditable_user_index'
@@ -152,6 +169,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
152
169
  name: '⭐ Star on GitHub',
153
170
  path: 'https://github.com/motor-admin/motor-admin-rails'
154
171
  }].to_json)
172
+
173
+ model.table_name = 'motor_api_configs'
174
+
175
+ model.create!(name: 'origin', path: '/')
155
176
  end
156
177
 
157
178
  def self.down
@@ -0,0 +1,80 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ class MotorApiConfig < ActiveRecord::Base
3
+ self.table_name = 'motor_api_configs'
4
+
5
+ encrypts :credentials if defined?(::Motor::EncryptedConfig)
6
+
7
+ serialize :credentials, Motor::HashSerializer
8
+ serialize :preferences, Motor::HashSerializer
9
+
10
+ attribute :preferences, default: -> { HashWithIndifferentAccess.new }
11
+ attribute :credentials, default: -> { HashWithIndifferentAccess.new }
12
+ end
13
+
14
+ class MotorForm < ActiveRecord::Base
15
+ self.table_name = 'motor_forms'
16
+ end
17
+
18
+ class MotorQuery < ActiveRecord::Base
19
+ self.table_name = 'motor_queries'
20
+
21
+ serialize :preferences, Motor::HashSerializer
22
+ end
23
+
24
+ def up
25
+ create_table :motor_api_configs do |t|
26
+ t.column :name, :string, null: false
27
+ t.column :url, :string, null: false
28
+ t.column :preferences, :text, null: false
29
+ t.column :credentials, :text, null: false
30
+ t.column :description, :text
31
+ t.column :deleted_at, :datetime
32
+
33
+ t.timestamps
34
+
35
+ t.index 'name',
36
+ name: 'motor_api_configs_name_unique_index',
37
+ unique: true,
38
+ where: 'deleted_at IS NULL'
39
+ end
40
+
41
+ add_column :motor_forms, :api_config_name, :string
42
+
43
+ MotorForm.all.each do |form|
44
+ if form.api_path.starts_with?('http')
45
+ url = form.api_path[%r{\Ahttps?://[^/]+}]
46
+
47
+ form.preferences[:default_values_api_path]&.delete(url)
48
+ form.update!(api_config_name: MotorApiConfig.find_or_create_by!(name: url, url: url).name,
49
+ api_path: form.api_path.delete(url))
50
+ else
51
+ form.update!(api_config_name: MotorApiConfig.find_or_create_by!(name: 'origin', url: '/').name)
52
+ end
53
+ end
54
+
55
+ MotorQuery.all.each do |query|
56
+ next if query.preferences['api_path'].blank?
57
+
58
+ if query.preferences['api_path'].starts_with?('http')
59
+ url = query.preferences['api_path'][%r{\Ahttps?://[^/]+}]
60
+
61
+ query.preferences['api_path'].delete(url)
62
+
63
+ query.preferences['api_config_name'] = MotorApiConfig.find_or_create_by!(name: url, url: url).name
64
+ else
65
+ query.preferences['api_config_name'] = MotorApiConfig.find_or_create_by!(name: 'origin', url: '/').name
66
+ end
67
+
68
+ query.save!
69
+ end
70
+
71
+ change_column_null :motor_forms, :api_config_name, false
72
+
73
+ MotorApiConfig.find_or_create_by!(name: 'origin', url: '/')
74
+ end
75
+
76
+ def down
77
+ remove_column :motor_forms, :api_config_name
78
+ drop_table :motor_api_configs
79
+ end
80
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+ require 'active_record'
6
+ require 'rails/generators/active_record'
7
+ require 'generators/motor/migration'
8
+
9
+ module Motor
10
+ module Generators
11
+ class UpgradeGenerator < Rails::Generators::Base
12
+ include Rails::Generators::Migration
13
+ extend Motor::Generators::Migration
14
+
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ def copy_migration
18
+ if Motor::ApiConfig.table_exists?
19
+ puts 'The latest Motor Admin features are already configured'
20
+ else
21
+ migration_template 'install_api_configs.rb', 'db/migrate/install_motor_api_configs.rb'
22
+
23
+ puts 'Run `rake db:migrate` to update DB schema'
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -53,7 +53,7 @@ module Motor
53
53
 
54
54
  type_map = connection_class.connection.send(:type_map)
55
55
 
56
- type_map.instance_variable_get('@mapping').map do |name, type|
56
+ type_map.instance_variable_get(:@mapping).map do |name, type|
57
57
  next unless name.is_a?(String)
58
58
 
59
59
  [type.call.class.to_s, name]
@@ -87,9 +87,9 @@ module Motor
87
87
 
88
88
  def name_already_exists?(alert)
89
89
  if alert.new_record?
90
- Alert.exists?(name: alert.name)
90
+ Alert.exists?(name: alert.name, deleted_at: nil)
91
91
  else
92
- Alert.exists?(['name = ? AND id != ?', alert.name, alert.id])
92
+ Alert.exists?(['name = ? AND id != ? AND deleted_at IS NULL', alert.name, alert.id])
93
93
  end
94
94
  end
95
95
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiConfigs
5
+ METHODS = %w[get post put delete].freeze
6
+
7
+ InvalidHttpMethod = Class.new(StandardError)
8
+
9
+ module_function
10
+
11
+ def run(api_config, method: nil, path: nil, body: nil, params: {}, headers: {})
12
+ method ||= 'get'
13
+
14
+ raise InvalidHttpMethod unless METHODS.include?(method.downcase)
15
+
16
+ Motor::NetHttpUtils.public_send(
17
+ method.downcase.to_sym,
18
+ api_config.url.to_s.sub(%r{/?\z}, '/') + path.delete_prefix('/'),
19
+ params,
20
+ headers.merge(api_config.headers),
21
+ body
22
+ )
23
+ end
24
+ end
25
+ end
@@ -26,7 +26,17 @@ module Motor
26
26
 
27
27
  return rel unless scope_configs
28
28
 
29
- ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
29
+ rel = ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
30
+
31
+ apply_order(rel, scope_configs[:preferences][:sort])
32
+ end
33
+
34
+ def apply_order(rel, params)
35
+ return rel if params.blank?
36
+
37
+ sort_key, sort_order = params.values_at(:key, :order)
38
+
39
+ rel.order(sort_key => sort_order)
30
40
  end
31
41
  end
32
42
  end
@@ -16,7 +16,7 @@ module Motor
16
16
 
17
17
  selected_columns ||= columns.first(SELECT_COLUMNS_AMOUNT)
18
18
 
19
- ([model.primary_key] + selected_columns).sort
19
+ ([model.primary_key] + selected_columns).compact.sort
20
20
  end
21
21
 
22
22
  def find_searchable_columns(model)
@@ -5,6 +5,7 @@ module Motor
5
5
  module BuildConfigsHash
6
6
  module_function
7
7
 
8
+ # rubocop:disable Metrics/AbcSize
8
9
  def call
9
10
  cache_keys = LoadFromCache.load_cache_keys
10
11
 
@@ -16,9 +17,11 @@ module Motor
16
17
  queries: build_queries_hash(cache_keys[:queries]),
17
18
  dashboards: build_dashboards_hash(cache_keys[:dashboards]),
18
19
  forms: build_forms_hash(cache_keys[:forms]),
19
- alerts: build_alerts_hash(cache_keys[:alerts])
20
+ alerts: build_alerts_hash(cache_keys[:alerts]),
21
+ api_configs: build_api_configs_hash(cache_keys[:api_configs])
20
22
  )
21
23
  end
24
+ # rubocop:enable Metrics/AbcSize
22
25
 
23
26
  def build_queries_hash(cache_key = nil)
24
27
  Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key).sort_by(&:id).map do |query|
@@ -43,11 +46,17 @@ module Motor
43
46
 
44
47
  def build_forms_hash(cache_key = nil)
45
48
  Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key).sort_by(&:id).map do |form|
46
- form.slice(%i[id name http_method api_path description preferences])
49
+ form.slice(%i[id name http_method api_path description preferences api_config_name])
47
50
  .merge(tags: form.tags.map(&:name), updated_at: form.updated_at.to_time)
48
51
  end
49
52
  end
50
53
 
54
+ def build_api_configs_hash(cache_key = nil)
55
+ Motor::Configs::LoadFromCache.load_api_configs(cache_key: cache_key).sort_by(&:id).map do |config|
56
+ config.slice(%i[id name url preferences description]).merge(updated_at: config.updated_at.to_time)
57
+ end
58
+ end
59
+
51
60
  def build_configs_hash(cache_key = nil)
52
61
  Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key).sort_by(&:key).map do |config|
53
62
  {
@@ -68,6 +68,12 @@ module Motor
68
68
  end
69
69
  end
70
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
+
71
77
  def maybe_fetch_from_cache(type, cache_key)
72
78
  return yield unless cache_key
73
79
 
@@ -95,7 +101,8 @@ module Motor
95
101
  Motor::Dashboard.select("'dashboards', MAX(updated_at)").to_sql,
96
102
  Motor::Alert.select("'alerts', MAX(updated_at)").to_sql,
97
103
  Motor::Query.select("'queries', MAX(updated_at)").to_sql,
98
- Motor::Form.select("'forms', 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
99
106
  ].join(' UNION ')
100
107
  end
101
108
  end
@@ -17,6 +17,7 @@ module Motor
17
17
  sync_forms(configs_hash)
18
18
  sync_configs(configs_hash)
19
19
  sync_resources(configs_hash)
20
+ sync_api_configs(configs_hash)
20
21
  end
21
22
  end
22
23
 
@@ -68,6 +69,28 @@ module Motor
68
69
  end
69
70
  end
70
71
 
72
+ def sync_api_configs(configs_hash)
73
+ return if configs_hash[:api_configs].blank?
74
+
75
+ configs_index = Motor::Configs::LoadFromCache.load_api_configs.index_by(&:name)
76
+
77
+ configs_hash[:api_configs].each do |attrs|
78
+ record = configs_index[attrs[:name]] || Motor::ApiConfig.new
79
+
80
+ next if record.updated_at && attrs[:updated_at] <= record.updated_at
81
+
82
+ record.update!(attrs.merge(deleted_at: nil))
83
+ end
84
+
85
+ archive_api_configs(configs_index, configs_hash[:api_configs])
86
+ end
87
+
88
+ def archive_api_configs(configs_index, api_configs)
89
+ configs_index.except(*api_configs.pluck('name')).each_value do |config|
90
+ config.update!(deleted_at: Time.current) if config.deleted_at.blank?
91
+ end
92
+ end
93
+
71
94
  def sync_resources(configs_hash)
72
95
  resources_index = Motor::Configs::LoadFromCache.load_resources.index_by(&:name)
73
96
 
@@ -63,9 +63,9 @@ module Motor
63
63
 
64
64
  def title_already_exists?(dashboard)
65
65
  if dashboard.new_record?
66
- Motor::Dashboard.exists?(title: dashboard.title)
66
+ Motor::Dashboard.exists?(title: dashboard.title, deleted_at: nil)
67
67
  else
68
- Motor::Dashboard.exists?(['title = ? AND id != ?', dashboard.title, dashboard.id])
68
+ Motor::Dashboard.exists?(['title = ? AND id != ? AND deleted_at IS NULL', dashboard.title, dashboard.id])
69
69
  end
70
70
  end
71
71
  end
@@ -38,6 +38,7 @@ module Motor
38
38
 
39
39
  ApplicationRecord.transaction do
40
40
  archive_with_existing_name(form) if force_replace
41
+ find_or_assign_api_config(form)
41
42
 
42
43
  form.save!
43
44
  end
@@ -50,12 +51,31 @@ module Motor
50
51
  end
51
52
 
52
53
  def assign_attributes(form, params)
53
- form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences))
54
+ form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences,
55
+ :api_config_name))
54
56
  form.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
55
57
 
58
+ find_or_assign_api_config(form)
59
+
56
60
  Motor::Tags.assign_tags(form, params[:tags])
57
61
  end
58
62
 
63
+ def find_or_assign_api_config(form)
64
+ return if form.api_config.present?
65
+
66
+ config = Motor::ApiConfig.find_by(url: [form.api_config_name, form.api_config_name.delete_suffix('/')])
67
+
68
+ if config
69
+ config.update!(deleted_at: nil)
70
+
71
+ form.api_config_name = config.name
72
+ else
73
+ form.api_config = Motor::ApiConfig.new(url: form.api_config_name.delete_suffix('/')).tap do |c|
74
+ c.name = c.url.sub(%r{\Ahttps?://}, '').delete_suffix('/')
75
+ end
76
+ end
77
+ end
78
+
59
79
  def archive_with_existing_name(form)
60
80
  Motor::Form.where(['name = ? AND id != ?', form.name, form.id])
61
81
  .update_all(deleted_at: Time.current)
@@ -63,9 +83,9 @@ module Motor
63
83
 
64
84
  def name_already_exists?(form)
65
85
  if form.new_record?
66
- Motor::Form.exists?(['name = ?', form.name])
86
+ Motor::Form.exists?(['name = ? AND deleted_at IS NULL', form.name])
67
87
  else
68
- Motor::Form.exists?(['name = ? AND id != ?', form.name, form.id])
88
+ Motor::Form.exists?(['name = ? AND id != ? AND deleted_at IS NULL', form.name, form.id])
69
89
  end
70
90
  end
71
91
  end
@@ -4,7 +4,7 @@ module Motor
4
4
  module NetHttpUtils
5
5
  module_function
6
6
 
7
- def get(url, params = {}, headers = {})
7
+ def get(url, params = {}, headers = {}, _body = nil)
8
8
  request = build_request(Net::HTTP::Get, url, params, headers, nil)
9
9
 
10
10
  execute_request(request)
@@ -16,9 +16,21 @@ module Motor
16
16
  execute_request(request)
17
17
  end
18
18
 
19
+ def put(url, params = {}, headers = {}, body = '')
20
+ request = build_request(Net::HTTP::Put, url, params, headers, body)
21
+
22
+ execute_request(request)
23
+ end
24
+
25
+ def delete(url, params = {}, headers = {}, body = '')
26
+ request = build_request(Net::HTTP::Delete, url, params, headers, body)
27
+
28
+ execute_request(request)
29
+ end
30
+
19
31
  def build_request(method_class, url, params, headers, body)
20
32
  uri = URI(url)
21
- uri.query = params.to_query
33
+ uri.query = params.to_query if params.present?
22
34
 
23
35
  request = method_class.new(uri)
24
36
  request.body = body if body.present?
@@ -38,6 +38,7 @@ module Motor
38
38
 
39
39
  ApplicationRecord.transaction do
40
40
  archive_with_existing_name(query) if force_replace
41
+ assign_or_create_api_config!(query)
41
42
 
42
43
  query.save!
43
44
  end
@@ -61,11 +62,27 @@ module Motor
61
62
  .update_all(deleted_at: Time.current)
62
63
  end
63
64
 
65
+ def assign_or_create_api_config!(query)
66
+ api_config_name = query.preferences[:api_config_name]
67
+
68
+ return if api_config_name.blank?
69
+ return if Motor::ApiConfig.find_by(name: api_config_name)
70
+
71
+ config = Motor::ApiConfig.find_by(url: [api_config_name, api_config_name.delete_suffix('/')])
72
+
73
+ config&.update!(deleted_at: nil)
74
+
75
+ config ||= Motor::ApiConfig.create!(name: api_config_name.sub(%r{\Ahttps?://}, '').delete_suffix('/'),
76
+ url: api_config_name.delete_suffix('/'))
77
+
78
+ query.preferences[:api_config_name] = config.name
79
+ end
80
+
64
81
  def name_already_exists?(query)
65
82
  if query.new_record?
66
- Query.exists?(name: query.name)
83
+ Query.exists?(name: query.name, deleted_at: nil)
67
84
  else
68
- Query.exists?(['name = ? AND id != ?', query.name, query.id])
85
+ Query.exists?(['name = ? AND id != ? AND deleted_at IS NULL', query.name, query.id])
69
86
  end
70
87
  end
71
88
  end
@@ -194,7 +194,9 @@ module Motor
194
194
  def normalize_statement_for_sql(statement)
195
195
  sql, _, attributes = statement
196
196
 
197
- sql = ActiveRecord::Base.sanitize_sql([sql.gsub(STATEMENT_VARIABLE_REGEXP, '?'), attributes.map(&:value)])
197
+ sql = ActiveRecord::Base.send(:replace_bind_variables,
198
+ sql.gsub(STATEMENT_VARIABLE_REGEXP, '?'),
199
+ attributes.map(&:value))
198
200
 
199
201
  [sql, 'SQL', attributes]
200
202
  end
@@ -41,7 +41,7 @@ module Motor
41
41
  def define_default_scope(klass, config)
42
42
  return klass if config[:custom_sql].blank?
43
43
 
44
- klass.instance_variable_set(:@__motor_custom_sql, config[:custom_sql].squish)
44
+ klass.instance_variable_set(:@__motor_custom_sql, config[:custom_sql].squish.delete_suffix(';'))
45
45
 
46
46
  klass.instance_eval do
47
47
  default_scope do
@@ -4,7 +4,8 @@ module Motor
4
4
  module Resources
5
5
  RESOURCE_ATTRS = %w[display_name display_column icon custom_sql visible display_primary_key
6
6
  searchable_columns].freeze
7
- COLUMN_ATTRS = %w[name display_name column_type access_type default_value reference virtual format validators].freeze
7
+ COLUMN_ATTRS = %w[name display_name column_type access_type default_value reference virtual format
8
+ validators].freeze
8
9
  ASSOCIATION_ATTRS = %w[name display_name model_name icon visible foreign_key primary_key options virtual
9
10
  polymorphic slug].freeze
10
11
  SCOPE_ATTRS = %w[name display_name scope_type preferences visible].freeze
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.2.37'
4
+ VERSION = '0.2.43'
5
5
  end
data/lib/motor.rb CHANGED
@@ -61,6 +61,7 @@ require 'motor/configs'
61
61
  require 'motor/queries'
62
62
  require 'motor/dashboards'
63
63
  require 'motor/forms'
64
+ require 'motor/api_configs'
64
65
  require 'motor/alerts'
65
66
  require 'motor/resources'
66
67
  require 'motor/hash_serializer'
@@ -2601,9 +2601,9 @@
2601
2601
  "icons/zoom-out.svg.gz": "icons/zoom-out.svg.gz",
2602
2602
  "icons/zoom-question.svg": "icons/zoom-question.svg",
2603
2603
  "icons/zoom-question.svg.gz": "icons/zoom-question.svg.gz",
2604
- "main-caf0e2eb1b4372864962.css.gz": "main-caf0e2eb1b4372864962.css.gz",
2605
- "main-caf0e2eb1b4372864962.js.LICENSE.txt": "main-caf0e2eb1b4372864962.js.LICENSE.txt",
2606
- "main-caf0e2eb1b4372864962.js.gz": "main-caf0e2eb1b4372864962.js.gz",
2607
- "main.css": "main-caf0e2eb1b4372864962.css",
2608
- "main.js": "main-caf0e2eb1b4372864962.js"
2604
+ "main-90db17316da397cbfa9c.css.gz": "main-90db17316da397cbfa9c.css.gz",
2605
+ "main-90db17316da397cbfa9c.js.LICENSE.txt": "main-90db17316da397cbfa9c.js.LICENSE.txt",
2606
+ "main-90db17316da397cbfa9c.js.gz": "main-90db17316da397cbfa9c.js.gz",
2607
+ "main.css": "main-90db17316da397cbfa9c.css",
2608
+ "main.js": "main-90db17316da397cbfa9c.js"
2609
2609
  }
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.2.37
4
+ version: 0.2.43
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-12-19 00:00:00.000000000 Z
11
+ date: 2021-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -114,6 +114,7 @@ files:
114
114
  - app/controllers/motor/active_storage_attachments_controller.rb
115
115
  - app/controllers/motor/alerts_controller.rb
116
116
  - app/controllers/motor/api_base_controller.rb
117
+ - app/controllers/motor/api_configs_controller.rb
117
118
  - app/controllers/motor/application_controller.rb
118
119
  - app/controllers/motor/assets_controller.rb
119
120
  - app/controllers/motor/audits_controller.rb
@@ -127,6 +128,7 @@ files:
127
128
  - app/controllers/motor/resource_default_queries_controller.rb
128
129
  - app/controllers/motor/resource_methods_controller.rb
129
130
  - app/controllers/motor/resources_controller.rb
131
+ - app/controllers/motor/run_api_requests_controller.rb
130
132
  - app/controllers/motor/run_queries_controller.rb
131
133
  - app/controllers/motor/schema_controller.rb
132
134
  - app/controllers/motor/send_alerts_controller.rb
@@ -139,6 +141,7 @@ files:
139
141
  - app/mailers/motor/application_mailer.rb
140
142
  - app/models/motor/alert.rb
141
143
  - app/models/motor/alert_lock.rb
144
+ - app/models/motor/api_config.rb
142
145
  - app/models/motor/application_record.rb
143
146
  - app/models/motor/audit.rb
144
147
  - app/models/motor/config.rb
@@ -158,6 +161,8 @@ files:
158
161
  - lib/generators/motor/install_generator.rb
159
162
  - lib/generators/motor/migration.rb
160
163
  - lib/generators/motor/templates/install.rb
164
+ - lib/generators/motor/templates/install_api_configs.rb
165
+ - lib/generators/motor/upgrade_generator.rb
161
166
  - lib/motor-admin.rb
162
167
  - lib/motor.rb
163
168
  - lib/motor/active_record_utils.rb
@@ -175,6 +180,7 @@ files:
175
180
  - lib/motor/alerts/persistance.rb
176
181
  - lib/motor/alerts/scheduled_alerts_cache.rb
177
182
  - lib/motor/alerts/scheduler.rb
183
+ - lib/motor/api_configs.rb
178
184
  - lib/motor/api_query.rb
179
185
  - lib/motor/api_query/apply_scope.rb
180
186
  - lib/motor/api_query/build_json.rb
@@ -1526,8 +1532,8 @@ files:
1526
1532
  - ui/dist/icons/zoom-money.svg.gz
1527
1533
  - ui/dist/icons/zoom-out.svg.gz
1528
1534
  - ui/dist/icons/zoom-question.svg.gz
1529
- - ui/dist/main-caf0e2eb1b4372864962.css.gz
1530
- - ui/dist/main-caf0e2eb1b4372864962.js.gz
1535
+ - ui/dist/main-90db17316da397cbfa9c.css.gz
1536
+ - ui/dist/main-90db17316da397cbfa9c.js.gz
1531
1537
  - ui/dist/manifest.json
1532
1538
  homepage:
1533
1539
  licenses: