motor-admin 0.1.55 → 0.1.61

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -13
  3. data/app/controllers/concerns/motor/current_ability.rb +21 -0
  4. data/app/controllers/concerns/motor/current_user_method.rb +8 -7
  5. data/app/controllers/motor/alerts_controller.rb +4 -4
  6. data/app/controllers/motor/api_base_controller.rb +2 -12
  7. data/app/controllers/motor/application_controller.rb +1 -0
  8. data/app/controllers/motor/audits_controller.rb +1 -1
  9. data/app/controllers/motor/auth_tokens_controller.rb +36 -0
  10. data/app/controllers/motor/configs_controller.rb +2 -2
  11. data/app/controllers/motor/dashboards_controller.rb +8 -4
  12. data/app/controllers/motor/data_controller.rb +9 -4
  13. data/app/controllers/motor/forms_controller.rb +4 -4
  14. data/app/controllers/motor/icons_controller.rb +2 -0
  15. data/app/controllers/motor/queries_controller.rb +4 -4
  16. data/app/controllers/motor/resource_methods_controller.rb +2 -0
  17. data/app/controllers/motor/resources_controller.rb +2 -2
  18. data/app/controllers/motor/run_queries_controller.rb +21 -1
  19. data/app/controllers/motor/ui_controller.rb +1 -1
  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 +1 -0
  23. data/lib/motor.rb +1 -0
  24. data/lib/motor/active_record_utils.rb +2 -1
  25. data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +13 -0
  26. data/lib/motor/active_record_utils/{active_record_filter.rb → active_record_filter_patch.rb} +0 -0
  27. data/lib/motor/admin.rb +21 -0
  28. data/lib/motor/api_query.rb +2 -2
  29. data/lib/motor/api_query/build_json.rb +101 -47
  30. data/lib/motor/api_query/filter.rb +2 -0
  31. data/lib/motor/api_query/search.rb +1 -0
  32. data/lib/motor/assets.rb +10 -1
  33. data/lib/motor/build_schema.rb +3 -1
  34. data/lib/motor/build_schema/active_storage_attachment_schema.rb +1 -0
  35. data/lib/motor/build_schema/apply_permissions.rb +50 -0
  36. data/lib/motor/build_schema/find_display_column.rb +1 -0
  37. data/lib/motor/build_schema/find_icon.rb +5 -1
  38. data/lib/motor/cancan_utils.rb +7 -0
  39. data/lib/motor/cancan_utils/ability_patch.rb +29 -0
  40. data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
  41. data/lib/motor/configs/build_ui_app_tag.rb +26 -16
  42. data/lib/motor/configs/load_from_cache.rb +20 -8
  43. data/lib/motor/queries/render_sql_template.rb +12 -2
  44. data/lib/motor/queries/run_query.rb +73 -19
  45. data/lib/motor/version.rb +1 -1
  46. data/ui/dist/main-fd0f75f789196ce24ffd.css.gz +0 -0
  47. data/ui/dist/main-fd0f75f789196ce24ffd.js.gz +0 -0
  48. data/ui/dist/manifest.json +5 -5
  49. metadata +14 -8
  50. data/app/controllers/motor/schemas_controller.rb +0 -11
  51. data/ui/dist/main-4d659b311d92611ad5f6.css.gz +0 -0
  52. data/ui/dist/main-4d659b311d92611ad5f6.js.gz +0 -0
@@ -20,7 +20,9 @@ module Motor
20
20
  private
21
21
 
22
22
  def render_result
23
- query_result = Queries::RunQuery.call(@query, variables_hash: params[:variables])
23
+ query_result = Queries::RunQuery.call(@query, variables_hash: variables_params,
24
+ limit: params[:limit].presence,
25
+ filters: filter_params)
24
26
 
25
27
  if query_result.error
26
28
  render json: { errors: [{ detail: query_result.error }] }, status: :unprocessable_entity
@@ -29,6 +31,16 @@ module Motor
29
31
  end
30
32
  end
31
33
 
34
+ def current_user_variables
35
+ return {} unless current_user
36
+
37
+ current_user
38
+ .attributes
39
+ .slice('id', 'email')
40
+ .transform_keys { |key| "current_user_#{key}" }
41
+ .compact
42
+ end
43
+
32
44
  def query_result_hash(query_result)
33
45
  {
34
46
  data: query_result.data,
@@ -45,5 +57,13 @@ module Motor
45
57
  def query_params
46
58
  params.require(:data).permit(:sql_body, preferences: {})
47
59
  end
60
+
61
+ def variables_params
62
+ params.fetch(:variables, {}).merge(current_user_variables)
63
+ end
64
+
65
+ def filter_params
66
+ (params[:filter] || params[:filters])&.to_unsafe_h
67
+ end
48
68
  end
49
69
  end
@@ -4,7 +4,7 @@ module Motor
4
4
  class UiController < ApplicationController
5
5
  layout 'motor/application'
6
6
 
7
- helper_method :current_user
7
+ helper_method :current_user, :current_ability
8
8
 
9
9
  def index
10
10
  render_ui
@@ -1 +1 @@
1
- <%= raw(Motor::Configs::BuildUiAppTag.call(current_user)) %>
1
+ <%= raw(Motor::Configs::BuildUiAppTag.call(current_user, current_ability)) %>
data/config/routes.rb CHANGED
@@ -5,6 +5,7 @@ Motor::Admin.routes.draw do
5
5
  scope 'api', as: :api do
6
6
  resources :run_queries, only: %i[show create]
7
7
  resources :send_alerts, only: %i[create]
8
+ resources :auth_tokens, only: %i[create]
8
9
  resources :queries, only: %i[index show create update destroy]
9
10
  resources :tags, only: %i[index]
10
11
  resources :configs, only: %i[index create]
@@ -149,6 +149,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
149
149
  drop_table :motor_audits
150
150
  drop_table :motor_alert_locks
151
151
  drop_table :motor_alerts
152
+ drop_table :motor_forms
152
153
  drop_table :motor_taggable_tags
153
154
  drop_table :motor_tags
154
155
  drop_table :motor_resources
data/lib/motor.rb CHANGED
@@ -53,6 +53,7 @@ require 'motor/version'
53
53
  require 'motor/admin'
54
54
  require 'motor/assets'
55
55
  require 'motor/active_record_utils'
56
+ require 'motor/cancan_utils'
56
57
  require 'motor/build_schema'
57
58
  require 'motor/api_query'
58
59
  require 'motor/tags'
@@ -20,4 +20,5 @@ require_relative './active_record_utils/fetch_methods'
20
20
  require_relative './active_record_utils/defined_scopes_extension'
21
21
  require_relative './active_record_utils/active_storage_links_extension'
22
22
  require_relative './active_record_utils/active_storage_blob_patch'
23
- require_relative './active_record_utils/active_record_filter'
23
+ require_relative './active_record_utils/active_record_filter_patch'
24
+ require_relative './active_record_utils/active_record_connection_column_patch'
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'active_record/connection_adapters/deduplicable'
5
+ rescue LoadError
6
+ nil
7
+ end
8
+
9
+ ActiveRecord::ConnectionAdapters::Column.class_eval do
10
+ def array
11
+ false
12
+ end
13
+ end
data/lib/motor/admin.rb CHANGED
@@ -45,6 +45,7 @@ module Motor
45
45
  initializer 'motor.alerts.scheduler' do
46
46
  config.after_initialize do
47
47
  next unless Motor.server?
48
+ next if Motor.development?
48
49
 
49
50
  Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
50
51
  end
@@ -65,10 +66,30 @@ module Motor
65
66
  end
66
67
  end
67
68
 
69
+ initializer 'warden.configure.dispatch_requests' do
70
+ next unless defined?(Warden::JWTAuth)
71
+
72
+ config.after_initialize do
73
+ Warden::JWTAuth.configure do |config|
74
+ config.dispatch_requests += [
75
+ ['POST', /\A#{Regexp.escape(Motor::Admin.routes.url_helpers.motor_api_auth_tokens_path)}\z/]
76
+ ]
77
+ end
78
+ end
79
+ end
80
+
68
81
  initializer 'motor.active_storage.extensions' do
69
82
  config.after_initialize do
70
83
  next unless defined?(ActiveStorage::Engine)
71
84
 
85
+ ActiveSupport.on_load(:active_storage_attachment) do
86
+ ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
87
+ end
88
+
89
+ ActiveSupport.on_load(:active_storage_blob) do
90
+ ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
91
+ end
92
+
72
93
  ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
73
94
  ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
74
95
  end
@@ -5,9 +5,9 @@ module Motor
5
5
  module_function
6
6
 
7
7
  def call(rel, params)
8
- rel = ApiQuery::Sort.call(rel, params[:sort])
8
+ rel = ApiQuery::Sort.call(rel, params[:sort] || params[:order])
9
9
  rel = ApiQuery::Paginate.call(rel, params[:page])
10
- rel = ApiQuery::Filter.call(rel, params[:filter])
10
+ rel = ApiQuery::Filter.call(rel, params[:filter] || params[:filters])
11
11
  rel = ApiQuery::ApplyScope.call(rel, params[:scope])
12
12
 
13
13
  ApiQuery::Search.call(rel, params[:q] || params[:search] || params[:query])
@@ -5,83 +5,90 @@ module Motor
5
5
  module BuildJson
6
6
  module_function
7
7
 
8
- def call(rel, params)
9
- rel = rel.none if params.dig(:page, :limit).yield_self { |limit| limit.present? && limit.to_i.zero? }
10
-
8
+ # @param rel [ActiveRecord::Base, ActiveRecord::Relation]
9
+ # @param params [Hash]
10
+ # @param current_ability [CanCan::Ability]
11
+ # @return [Hash]
12
+ def call(rel, params, current_ability = Motor::CancanUtils::CanManageAll.new)
13
+ rel = rel.none if limit_zero_params?(params)
11
14
  rel = rel.preload_associations_lazily if rel.is_a?(ActiveRecord::Relation)
12
15
 
13
- json_params = {}.with_indifferent_access
14
-
15
- assign_include_params(json_params, rel, params)
16
- assign_fields_params(json_params, rel, params)
17
-
18
- rel.as_json(json_params)
19
- end
16
+ model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
20
17
 
21
- def assign_include_params(json_params, _rel, api_params)
22
- return if api_params['include'].blank?
18
+ include_hash = build_include_hash(params['include'])
19
+ models_index = build_models_index(model, include_hash)
23
20
 
24
- include_params = api_params['include']
21
+ json_params = normalize_include_params(include_hash)
25
22
 
26
- if include_params.is_a?(String)
27
- include_params =
28
- include_params.split(',').reduce({}) do |accumulator, path|
29
- hash = {}
23
+ assign_fields_params!(json_params, model, params, current_ability, models_index)
30
24
 
31
- path.split('.').reduce(hash) do |acc, part|
32
- acc_hash = {}
33
-
34
- acc[part] = acc_hash
25
+ rel.as_json(json_params.with_indifferent_access)
26
+ end
35
27
 
36
- acc_hash
37
- end
28
+ # @param include_params [Hash]
29
+ # @return [Hash]
30
+ def build_include_hash(include_params)
31
+ return {} if include_params.blank?
38
32
 
39
- accumulator.deep_merge(hash)
40
- end
33
+ if include_params.is_a?(String)
34
+ build_hash_from_string_path(include_params)
35
+ else
36
+ include_params
41
37
  end
42
-
43
- json_params.deep_merge!(normalize_include_params(include_params))
44
38
  end
45
39
 
46
- def assign_fields_params(json_params, rel, params)
40
+ # @param json_params [Hash]
41
+ # @param model [Class<ActiveRecord::Base>]
42
+ # @param params [Hash]
43
+ # @param current_ability [CanCan::Ability]
44
+ # @param models_index [Hash]
45
+ # @return [void]
46
+ def assign_fields_params!(json_params, model, params, current_ability, models_index)
47
47
  return if params[:fields].blank?
48
48
 
49
- model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
50
-
51
49
  params[:fields].each do |key, fields|
52
- fields = fields.split(',') if fields.is_a?(String)
53
-
54
- merge_fields_params!(json_params, key, fields, model)
55
- end
56
- end
50
+ fields_model = models_index[key]
57
51
 
58
- def merge_fields_params!(json_params, key, fields, model)
59
- model_name = model.name.underscore
52
+ next unless model
60
53
 
61
- if key == model_name || model_name.split('/').last == key
62
- json_params.merge!(build_fields_hash(model, fields))
63
- else
64
- hash = find_key_in_params(json_params, key)
54
+ fields = fields.split(',') if fields.is_a?(String)
65
55
 
66
- fields_hash = build_fields_hash(model.reflections[key]&.klass, fields)
56
+ fields_hash = fields_model == model ? json_params : find_key_in_params(json_params, key)
67
57
 
68
- hash.merge!(fields_hash)
58
+ fields_hash.merge!(build_fields_hash(fields_model, fields, current_ability))
69
59
  end
70
60
  end
71
61
 
72
- def build_fields_hash(model, fields)
73
- columns = model ? model.columns.map(&:name) : []
62
+ # @param model [Class<ActiveRecord::Base>]
63
+ # @param fields [Hash]
64
+ # @param current_ability [CanCan::Ability]
65
+ # @return [Hash]
66
+ def build_fields_hash(model, fields, current_ability)
67
+ return { 'methods' => fields } unless model
68
+
69
+ column_names = model.column_names.map(&:to_sym)
70
+ instance_methods = model.instance_methods
71
+ permitted_attributes = current_ability.permitted_attributes(:read, model)
72
+ is_permitted_all = column_names == permitted_attributes
73
+
74
74
  fields_hash = { 'only' => [], 'methods' => [] }
75
75
 
76
76
  fields.each_with_object(fields_hash) do |field, acc|
77
- if field.in?(columns)
77
+ field_symbol = field.to_sym
78
+
79
+ next if !is_permitted_all && permitted_attributes.exclude?(field_symbol)
80
+
81
+ if column_names.include?(field_symbol)
78
82
  acc['only'] << field
79
- elsif model.nil? || model.instance_methods.include?(field.to_sym)
83
+ elsif instance_methods.include?(field_symbol)
80
84
  acc['methods'] << field
81
85
  end
82
86
  end
83
87
  end
84
88
 
89
+ # @param params [Hash]
90
+ # @param key [String]
91
+ # @return [Hash]
85
92
  def find_key_in_params(params, key)
86
93
  params = params['include']
87
94
 
@@ -93,6 +100,8 @@ module Motor
93
100
  end
94
101
  end
95
102
 
103
+ # @param params [Hash]
104
+ # @return [Hash]
96
105
  def normalize_include_params(params)
97
106
  case params
98
107
  when Array
@@ -112,6 +121,51 @@ module Motor
112
121
  raise ArgumentError, "Wrong include param type #{params.class}"
113
122
  end
114
123
  end
124
+
125
+ # @param model [Class<ActiveRecord::Base>]
126
+ # @param includes_hash [Hash]
127
+ # @return [Hash]
128
+ def build_models_index(model, includes_hash)
129
+ default_index = {
130
+ model.name.underscore => model,
131
+ model.name.underscore.split('/').last => model
132
+ }
133
+
134
+ includes_hash.reduce(default_index) do |acc, (key, value)|
135
+ reflection = model.reflections[key]
136
+
137
+ next acc unless reflection
138
+ next acc if reflection.polymorphic?
139
+
140
+ acc[key] = reflection.klass
141
+
142
+ acc.merge(build_models_index(reflection.klass, value))
143
+ end
144
+ end
145
+
146
+ # @param string_path [String]
147
+ # @return [Hash]
148
+ def build_hash_from_string_path(string_path)
149
+ string_path.split(',').reduce({}) do |accumulator, path|
150
+ hash = {}
151
+
152
+ path.split('.').reduce(hash) do |acc, part|
153
+ acc_hash = {}
154
+
155
+ acc[part] = acc_hash
156
+
157
+ acc_hash
158
+ end
159
+
160
+ accumulator.deep_merge(hash)
161
+ end
162
+ end
163
+
164
+ # @param params [Hash]
165
+ # @return [Boolean]
166
+ def limit_zero_params?(params)
167
+ params.dig(:page, :limit).yield_self { |limit| limit.present? && limit.to_i.zero? }
168
+ end
115
169
  end
116
170
  end
117
171
  end
@@ -49,6 +49,8 @@ module Motor
49
49
 
50
50
  def normalize_action(action, value)
51
51
  case action
52
+ when 'includes'
53
+ ['contains', value]
52
54
  when 'contains'
53
55
  ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1%')]
54
56
  when 'starts_with'
@@ -57,6 +57,7 @@ module Motor
57
57
  def find_searchable_columns(model)
58
58
  model.columns.map do |column|
59
59
  next unless column.type.in?(COLUMN_TYPES)
60
+ next if column.respond_to?(:array?) && column.array?
60
61
  next if model.validators_on(column.name).any?(ActiveModel::Validations::InclusionValidator)
61
62
 
62
63
  column.name
data/lib/motor/assets.rb CHANGED
@@ -8,10 +8,19 @@ module Motor
8
8
  MANIFEST_PATH = ASSETS_PATH.join('manifest.json')
9
9
  DEV_SERVER_URL = 'http://localhost:9090/'
10
10
 
11
+ CACHE_STORE =
12
+ if Rails.env.production?
13
+ ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
14
+ else
15
+ ActiveSupport::Cache::NullStore.new
16
+ end
17
+
11
18
  module_function
12
19
 
13
20
  def manifest
14
- JSON.parse(MANIFEST_PATH.read)
21
+ CACHE_STORE.fetch('manifest') do
22
+ JSON.parse(MANIFEST_PATH.read)
23
+ end
15
24
  end
16
25
 
17
26
  def icons
@@ -58,9 +58,10 @@ module Motor
58
58
 
59
59
  module_function
60
60
 
61
- def call(cache_keys = {})
61
+ def call(cache_keys = {}, current_ability = nil)
62
62
  schema = LoadFromRails.call
63
63
  schema = MergeSchemaConfigs.call(schema, cache_keys)
64
+ schema = ApplyPermissions.call(schema, current_ability) if current_ability
64
65
 
65
66
  ReorderSchema.call(schema, cache_keys)
66
67
  end
@@ -75,4 +76,5 @@ require_relative './build_schema/find_icon'
75
76
  require_relative './build_schema/persist_resource_configs'
76
77
  require_relative './build_schema/reorder_schema'
77
78
  require_relative './build_schema/merge_schema_configs'
79
+ require_relative './build_schema/apply_permissions'
78
80
  require_relative './build_schema/utils'
@@ -5,6 +5,7 @@ module Motor
5
5
  ACTIVE_STORAGE_ATTACHMENT_SCHEMA = {
6
6
  name: 'active_storage/attachment',
7
7
  slug: 'active_storage__attachments',
8
+ class_name: 'ActiveStorage::Attachment',
8
9
  table_name: 'active_storage_attachments',
9
10
  primary_key: 'id',
10
11
  display_name: 'Attachments',
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module ApplyPermissions
6
+ module_function
7
+
8
+ def call(schema, ability)
9
+ schema.map do |model|
10
+ klass = model[:class_name].constantize
11
+
12
+ next unless ability.can?(:read, klass)
13
+
14
+ model[:associations] = filter_associations(model[:associations], ability)
15
+ model[:columns] = filter_columns(klass, model[:columns], ability)
16
+ model[:actions] = filter_actions(klass, model[:actions], ability)
17
+
18
+ model
19
+ end.compact
20
+ end
21
+
22
+ def filter_associations(associations, ability)
23
+ associations.select do |assoc|
24
+ ability.can?(:read, assoc[:model_name].classify.constantize)
25
+ end
26
+ end
27
+
28
+ def filter_columns(model, columns, ability)
29
+ columns.map do |column|
30
+ next unless ability.can?(:read, model, column[:name])
31
+
32
+ next if column.dig(:reference, :model_name).present? &&
33
+ !ability.can?(:read, column[:reference][:model_name].classify.constantize)
34
+
35
+ unless ability.can?(:update, model, column[:name])
36
+ column = column.merge(access_type: BuildSchema::ColumnAccessTypes::READ_ONLY)
37
+ end
38
+
39
+ column
40
+ end.compact
41
+ end
42
+
43
+ def filter_actions(model, actions, ability)
44
+ actions.select do |action|
45
+ ability.can?(action[:name].to_sym, model)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end