motor-admin 0.1.56 → 0.1.62
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.
- checksums.yaml +4 -4
- data/README.md +9 -13
- data/app/controllers/concerns/motor/current_ability.rb +21 -0
- data/app/controllers/concerns/motor/current_user_method.rb +8 -7
- data/app/controllers/motor/alerts_controller.rb +4 -4
- data/app/controllers/motor/api_base_controller.rb +2 -12
- data/app/controllers/motor/application_controller.rb +1 -0
- data/app/controllers/motor/audits_controller.rb +1 -1
- data/app/controllers/motor/auth_tokens_controller.rb +36 -0
- data/app/controllers/motor/configs_controller.rb +2 -2
- data/app/controllers/motor/dashboards_controller.rb +8 -4
- data/app/controllers/motor/data_controller.rb +9 -4
- data/app/controllers/motor/forms_controller.rb +4 -4
- data/app/controllers/motor/icons_controller.rb +2 -0
- data/app/controllers/motor/queries_controller.rb +4 -4
- data/app/controllers/motor/resource_methods_controller.rb +2 -0
- data/app/controllers/motor/resources_controller.rb +2 -2
- data/app/controllers/motor/run_queries_controller.rb +21 -1
- data/app/controllers/motor/ui_controller.rb +1 -1
- data/app/views/motor/ui/show.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/lib/generators/motor/templates/install.rb +1 -0
- data/lib/motor.rb +1 -0
- data/lib/motor/active_record_utils.rb +2 -1
- data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +13 -0
- data/lib/motor/active_record_utils/{active_record_filter.rb → active_record_filter_patch.rb} +0 -0
- data/lib/motor/admin.rb +21 -0
- data/lib/motor/api_query.rb +2 -2
- data/lib/motor/api_query/build_json.rb +101 -47
- data/lib/motor/api_query/filter.rb +11 -1
- data/lib/motor/assets.rb +10 -1
- data/lib/motor/build_schema.rb +3 -1
- data/lib/motor/build_schema/active_storage_attachment_schema.rb +1 -0
- data/lib/motor/build_schema/apply_permissions.rb +50 -0
- data/lib/motor/build_schema/find_icon.rb +5 -1
- data/lib/motor/cancan_utils.rb +7 -0
- data/lib/motor/cancan_utils/ability_patch.rb +29 -0
- data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
- data/lib/motor/configs/build_ui_app_tag.rb +26 -16
- data/lib/motor/configs/load_from_cache.rb +20 -8
- data/lib/motor/queries/render_sql_template.rb +12 -2
- data/lib/motor/queries/run_query.rb +73 -19
- data/lib/motor/version.rb +1 -1
- data/ui/dist/main-fd0f75f789196ce24ffd.css.gz +0 -0
- data/ui/dist/main-fd0f75f789196ce24ffd.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +14 -8
- data/app/controllers/motor/schemas_controller.rb +0 -11
- data/ui/dist/main-1f811bd0c7c600a0e204.css.gz +0 -0
- data/ui/dist/main-1f811bd0c7c600a0e204.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:
|
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
|
@@ -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
@@ -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/
|
23
|
+
require_relative './active_record_utils/active_record_filter_patch'
|
24
|
+
require_relative './active_record_utils/active_record_connection_column_patch'
|
data/lib/motor/active_record_utils/{active_record_filter.rb → active_record_filter_patch.rb}
RENAMED
File without changes
|
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
|
data/lib/motor/api_query.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
18
|
+
include_hash = build_include_hash(params['include'])
|
19
|
+
models_index = build_models_index(model, include_hash)
|
23
20
|
|
24
|
-
|
21
|
+
json_params = normalize_include_params(include_hash)
|
25
22
|
|
26
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
acc[part] = acc_hash
|
25
|
+
rel.as_json(json_params.with_indifferent_access)
|
26
|
+
end
|
35
27
|
|
36
|
-
|
37
|
-
|
28
|
+
# @param include_params [Hash]
|
29
|
+
# @return [Hash]
|
30
|
+
def build_include_hash(include_params)
|
31
|
+
return {} if include_params.blank?
|
38
32
|
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
merge_fields_params!(json_params, key, fields, model)
|
55
|
-
end
|
56
|
-
end
|
50
|
+
fields_model = models_index[key]
|
57
51
|
|
58
|
-
|
59
|
-
model_name = model.name.underscore
|
52
|
+
next unless model
|
60
53
|
|
61
|
-
|
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 =
|
56
|
+
fields_hash = fields_model == model ? json_params : find_key_in_params(json_params, key)
|
67
57
|
|
68
|
-
|
58
|
+
fields_hash.merge!(build_fields_hash(fields_model, fields, current_ability))
|
69
59
|
end
|
70
60
|
end
|
71
61
|
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
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
|
@@ -4,6 +4,7 @@ module Motor
|
|
4
4
|
module ApiQuery
|
5
5
|
module Filter
|
6
6
|
LIKE_FILTER_VALUE_REGEXP = /\A%?(.*?)%?\z/.freeze
|
7
|
+
DISTINCT_DESTRICTED_COLUMN_TYPES = %i[json point].freeze
|
7
8
|
|
8
9
|
module_function
|
9
10
|
|
@@ -12,7 +13,10 @@ module Motor
|
|
12
13
|
|
13
14
|
normalized_params = normalize_params(Array.wrap(params))
|
14
15
|
|
15
|
-
rel.filter(normalized_params)
|
16
|
+
rel = rel.filter(normalized_params)
|
17
|
+
rel = rel.distinct if can_apply_distinct?(rel)
|
18
|
+
|
19
|
+
rel
|
16
20
|
end
|
17
21
|
|
18
22
|
def normalize_params(params)
|
@@ -47,6 +51,12 @@ module Motor
|
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
54
|
+
def can_apply_distinct?(rel)
|
55
|
+
rel.columns.none? do |column|
|
56
|
+
DISTINCT_DESTRICTED_COLUMN_TYPES.include?(column.type)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
50
60
|
def normalize_action(action, value)
|
51
61
|
case action
|
52
62
|
when 'includes'
|
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
|
-
|
21
|
+
CACHE_STORE.fetch('manifest') do
|
22
|
+
JSON.parse(MANIFEST_PATH.read)
|
23
|
+
end
|
15
24
|
end
|
16
25
|
|
17
26
|
def icons
|
data/lib/motor/build_schema.rb
CHANGED
@@ -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'
|
@@ -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
|