motor-admin 0.1.23 → 0.1.31
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/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
- data/app/controllers/motor/active_storage_attachments_controller.rb +18 -6
- data/app/controllers/motor/data_controller.rb +14 -13
- data/app/views/motor/alerts_mailer/alert_email.html.erb +1 -1
- data/config/routes.rb +1 -12
- data/lib/generators/motor/install_generator.rb +2 -0
- data/lib/motor.rb +1 -1
- data/lib/motor/active_record_utils/types.rb +1 -2
- data/lib/motor/admin.rb +34 -0
- data/lib/motor/api_query/build_json.rb +17 -10
- data/lib/motor/api_query/sort.rb +5 -1
- data/lib/motor/build_schema/load_from_rails.rb +73 -50
- data/lib/motor/build_schema/merge_schema_configs.rb +47 -6
- data/lib/motor/build_schema/persist_resource_configs.rb +15 -36
- data/lib/motor/build_schema/reorder_schema.rb +22 -14
- data/lib/motor/queries.rb +1 -0
- data/lib/motor/queries/render_sql_template.rb +51 -0
- data/lib/motor/queries/run_query.rb +50 -22
- data/lib/motor/ui_configs.rb +33 -13
- data/lib/motor/version.rb +1 -1
- data/ui/dist/{main-a32a143c00ba97ad5bf9.css.gz → main-b27ee7bbbc5e97fb1743.css.gz} +0 -0
- data/ui/dist/main-b27ee7bbbc5e97fb1743.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +6 -5
- data/lib/motor/api.rb +0 -6
- data/ui/dist/main-a32a143c00ba97ad5bf9.js.gz +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34d4ac091383f79174302c02c385c3757f5aa34f3903339579268c5b1f8be6f4
|
4
|
+
data.tar.gz: 5311fb94097abbb727371002c44a019bca9b84b60d9ba3271a3e4029adbe6f5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 938d8910276a5ea54df6254cdd6d7964c5f34ceb7e87fe8c2aa76227dab5bc4a1d03653c9450012d2d013ff03e9f70163644c3530fd9ea23af6c98941c531561
|
7
|
+
data.tar.gz: f94105d6c8c14225fac6f3e341aef95399a8723d958f2d34e7715c05593d993bdda1cc2674b6d32a74216507271d0d559de3f221ba49a9b9b9d769d6c090f78b
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module WrapIoParams
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :wrap_io_params, only: %i[update create]
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def wrap_io_params(hash = params)
|
14
|
+
hash.each do |key, value|
|
15
|
+
if key == 'io'
|
16
|
+
hash[key] = StringIO.new(value.encode('ISO-8859-1'))
|
17
|
+
elsif value.is_a?(ActionController::Parameters)
|
18
|
+
wrap_io_params(value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
hash
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -2,16 +2,15 @@
|
|
2
2
|
|
3
3
|
module Motor
|
4
4
|
class ActiveStorageAttachmentsController < ApiBaseController
|
5
|
+
include Motor::WrapIoParams
|
6
|
+
|
5
7
|
wrap_parameters :data, except: %i[include fields]
|
6
8
|
|
7
9
|
load_and_authorize_resource :attachment, class: 'ActiveStorage::Attachment', parent: false
|
8
10
|
|
9
11
|
def create
|
10
|
-
if
|
11
|
-
@attachment.record.public_send(@attachment.name).attach(
|
12
|
-
io: StringIO.new(params.dig(:data, :file, :io).to_s.encode('ISO-8859-1')),
|
13
|
-
filename: params.dig(:data, :file, :filename)
|
14
|
-
)
|
12
|
+
if attachable?(@attachment.record)
|
13
|
+
@attachment.record.public_send(@attachment.name).attach(file_params)
|
15
14
|
|
16
15
|
head :ok
|
17
16
|
else
|
@@ -21,8 +20,21 @@ module Motor
|
|
21
20
|
|
22
21
|
private
|
23
22
|
|
23
|
+
def attachable?(record)
|
24
|
+
record.respond_to?("#{@attachment.name}_attachment=") ||
|
25
|
+
record.respond_to?("#{@attachment.name}_attachments=")
|
26
|
+
end
|
27
|
+
|
28
|
+
def file_params
|
29
|
+
params.require(:data).require(:file).permit(:io, :filename).to_h
|
30
|
+
end
|
31
|
+
|
24
32
|
def attachment_params
|
25
|
-
params
|
33
|
+
if params[:data].present?
|
34
|
+
params.require(:data).except(:file).permit!
|
35
|
+
else
|
36
|
+
{}
|
37
|
+
end
|
26
38
|
end
|
27
39
|
end
|
28
40
|
end
|
@@ -2,13 +2,14 @@
|
|
2
2
|
|
3
3
|
module Motor
|
4
4
|
class DataController < ApiBaseController
|
5
|
+
include Motor::WrapIoParams
|
6
|
+
|
5
7
|
INSTANCE_VARIABLE_NAME = 'resource'
|
6
8
|
|
7
9
|
wrap_parameters :data, except: %i[include fields]
|
8
10
|
|
9
11
|
before_action :load_and_authorize_resource
|
10
12
|
before_action :load_and_authorize_association
|
11
|
-
before_action :wrap_io_params
|
12
13
|
|
13
14
|
def index
|
14
15
|
@resources = Motor::ApiQuery.call(@resources, params)
|
@@ -27,12 +28,20 @@ module Motor
|
|
27
28
|
@resource.save!
|
28
29
|
|
29
30
|
render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
|
31
|
+
rescue ActiveRecord::RecordInvalid
|
32
|
+
render json: { errors: @resource.errors }, status: :unprocessable_entity
|
33
|
+
rescue StandardError => e
|
34
|
+
render json: { errors: [e.message] }, status: :unprocessable_entity
|
30
35
|
end
|
31
36
|
|
32
37
|
def update
|
33
38
|
@resource.update!(resource_params)
|
34
39
|
|
35
40
|
render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
|
41
|
+
rescue ActiveRecord::RecordInvalid
|
42
|
+
render json: { errors: @resource.errors }, status: :unprocessable_entity
|
43
|
+
rescue StandardError => e
|
44
|
+
render json: { errors: [e.message] }, status: :unprocessable_entity
|
36
45
|
end
|
37
46
|
|
38
47
|
def destroy
|
@@ -77,6 +86,8 @@ module Motor
|
|
77
86
|
self,
|
78
87
|
options
|
79
88
|
).load_and_authorize_resource
|
89
|
+
rescue StandardError => e
|
90
|
+
render json: { errors: [e.message] }, status: :unprocessable_entity
|
80
91
|
end
|
81
92
|
|
82
93
|
def load_and_authorize_association
|
@@ -96,6 +107,8 @@ module Motor
|
|
96
107
|
else
|
97
108
|
render json: { message: 'Unknown association' }, status: :not_found
|
98
109
|
end
|
110
|
+
rescue StandardError => e
|
111
|
+
render json: { errors: [e.message] }, status: :unprocessable_entity
|
99
112
|
end
|
100
113
|
|
101
114
|
def resource_params
|
@@ -105,17 +118,5 @@ module Motor
|
|
105
118
|
{}
|
106
119
|
end
|
107
120
|
end
|
108
|
-
|
109
|
-
def wrap_io_params(hash = params)
|
110
|
-
hash.each do |key, value|
|
111
|
-
if key == 'io'
|
112
|
-
hash[key] = StringIO.new(value.encode('ISO-8859-1'))
|
113
|
-
elsif value.is_a?(ActionController::Parameters)
|
114
|
-
wrap_io_params(value)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
hash
|
119
|
-
end
|
120
121
|
end
|
121
122
|
end
|
@@ -77,7 +77,7 @@
|
|
77
77
|
<% if @query_result.data.length > 1 %>
|
78
78
|
<p style="margin: 7px 0; float: left">Showing <%= @query_result.data.first(40).size %> of <%= @query_result.data.size %> rows</p>
|
79
79
|
<% end %>
|
80
|
-
<a href="<%= Motor::Admin.routes.url_helpers.motor_ui_query_url(@alert.query, { host: 'localhost:3000'}.merge(Rails.application.config.action_mailer.default_url_options || {})) %>" target="blank" style="margin: 7px 0; float: right">Open query </a>
|
80
|
+
<a href="<%= Motor::Admin.routes.url_helpers.motor_ui_query_url(@alert.query, { host: 'localhost:3000' }.merge(Rails.application.config.action_mailer.default_url_options || {})) %>" target="blank" style="margin: 7px 0; float: right">Open query </a>
|
81
81
|
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
|
82
82
|
<tr>
|
83
83
|
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px; max-width: 0px; overflow: scroll">
|
data/config/routes.rb
CHANGED
@@ -47,15 +47,4 @@ Motor::Admin.routes.draw do
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
|
-
namespace :motor, path: '' do
|
52
|
-
resources :resources, path: '/:resource',
|
53
|
-
only: %i[index show update create destroy],
|
54
|
-
controller: 'data' do
|
55
|
-
put '/:method', to: 'data#execute'
|
56
|
-
resources :association, path: '/:association',
|
57
|
-
only: %i[index create],
|
58
|
-
controller: 'data'
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
50
|
+
ActiveSupport::Notifications.instrument('motor.routes.loaded')
|
data/lib/motor.rb
CHANGED
data/lib/motor/admin.rb
CHANGED
@@ -2,6 +2,25 @@
|
|
2
2
|
|
3
3
|
module Motor
|
4
4
|
class Admin < ::Rails::Engine
|
5
|
+
initializer 'motor.startup_message' do
|
6
|
+
ActiveSupport::Notifications.subscribe('motor.routes.loaded') do
|
7
|
+
next unless Motor.server?
|
8
|
+
|
9
|
+
if Rails.application.routes.url_helpers.respond_to?(:motor_admin_path)
|
10
|
+
url =
|
11
|
+
begin
|
12
|
+
Rails.application.routes.url_helpers.motor_admin_url
|
13
|
+
rescue ArgumentError
|
14
|
+
Rails.application.routes.url_helpers.motor_admin_path
|
15
|
+
end
|
16
|
+
|
17
|
+
puts
|
18
|
+
puts "⚡ Motor Admin is starting under #{url}"
|
19
|
+
puts
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
5
24
|
initializer 'motor.filter_params' do
|
6
25
|
Rails.application.config.filter_parameters += %i[io]
|
7
26
|
end
|
@@ -14,6 +33,21 @@ module Motor
|
|
14
33
|
end
|
15
34
|
end
|
16
35
|
|
36
|
+
initializer 'motor.basic_auth' do
|
37
|
+
next if ENV['MOTOR_AUTH_PASSWORD'].blank?
|
38
|
+
|
39
|
+
config.middleware.use Rack::Auth::Basic do |username, password|
|
40
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
41
|
+
::Digest::SHA256.hexdigest(username),
|
42
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_USERNAME'].to_s)
|
43
|
+
) &
|
44
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
45
|
+
::Digest::SHA256.hexdigest(password),
|
46
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_PASSWORD'].to_s)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
17
51
|
initializer 'motor.active_storage.extensions' do
|
18
52
|
ActiveSupport.on_load(:active_storage_attachment) do
|
19
53
|
ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
|
@@ -29,9 +29,11 @@ module Motor
|
|
29
29
|
hash = {}
|
30
30
|
|
31
31
|
path.split('.').reduce(hash) do |acc, part|
|
32
|
-
|
32
|
+
acc_hash = {}
|
33
33
|
|
34
|
-
acc[part]
|
34
|
+
acc[part] = acc_hash
|
35
|
+
|
36
|
+
acc_hash
|
35
37
|
end
|
36
38
|
|
37
39
|
accumulator.deep_merge(hash)
|
@@ -45,20 +47,25 @@ module Motor
|
|
45
47
|
return if params[:fields].blank?
|
46
48
|
|
47
49
|
model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
|
48
|
-
model_name = model.name.underscore
|
49
50
|
|
50
51
|
params[:fields].each do |key, fields|
|
51
52
|
fields = fields.split(',') if fields.is_a?(String)
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
hash = find_key_in_params(json_params, key)
|
54
|
+
merge_fields_params!(key, fields, json_params, model)
|
55
|
+
end
|
56
|
+
end
|
57
57
|
|
58
|
-
|
58
|
+
def merge_fields_params!(key, fields, json_params, model)
|
59
|
+
model_name = model.name.underscore
|
59
60
|
|
60
|
-
|
61
|
-
|
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)
|
65
|
+
|
66
|
+
fields_hash = build_fields_hash(model.reflections[key]&.klass, fields)
|
67
|
+
|
68
|
+
hash.merge!(fields_hash)
|
62
69
|
end
|
63
70
|
end
|
64
71
|
|
data/lib/motor/api_query/sort.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module Motor
|
4
4
|
module BuildSchema
|
5
5
|
module LoadFromRails
|
6
|
+
MUTEX = Mutex.new
|
7
|
+
|
6
8
|
module_function
|
7
9
|
|
8
10
|
def call
|
@@ -82,22 +84,26 @@ module Motor
|
|
82
84
|
model.columns.map do |column|
|
83
85
|
next if reference_columns.find { |c| c[:name] == column.name }
|
84
86
|
|
85
|
-
|
86
|
-
name: column.name,
|
87
|
-
display_name: column.name.humanize,
|
88
|
-
column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
|
89
|
-
access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
|
90
|
-
default_value: default_attrs[column.name],
|
91
|
-
validators: fetch_validators(model, column.name),
|
92
|
-
reference: nil,
|
93
|
-
format: {},
|
94
|
-
virtual: false
|
95
|
-
}
|
87
|
+
build_table_column(column, model, default_attrs)
|
96
88
|
end.compact
|
97
89
|
|
98
90
|
reference_columns + table_columns
|
99
91
|
end
|
100
92
|
|
93
|
+
def build_table_column(column, model, default_attrs)
|
94
|
+
{
|
95
|
+
name: column.name,
|
96
|
+
display_name: column.name.humanize,
|
97
|
+
column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
|
98
|
+
access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
|
99
|
+
default_value: default_attrs[column.name],
|
100
|
+
validators: fetch_validators(model, column.name),
|
101
|
+
reference: nil,
|
102
|
+
format: {},
|
103
|
+
virtual: false
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
101
107
|
def fetch_reference_columns(model)
|
102
108
|
default_attrs = model.new.attributes
|
103
109
|
|
@@ -110,32 +116,36 @@ module Motor
|
|
110
116
|
next
|
111
117
|
end
|
112
118
|
|
113
|
-
column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
|
114
|
-
|
115
119
|
next if ref.klass.name == 'ActiveStorage::Blob'
|
116
120
|
|
117
|
-
|
118
|
-
|
119
|
-
{
|
120
|
-
name: column_name,
|
121
|
-
display_name: column_name.humanize,
|
122
|
-
column_type: is_attachment ? 'file' : 'integer',
|
123
|
-
access_type: ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY,
|
124
|
-
default_value: default_attrs[column_name],
|
125
|
-
validators: fetch_validators(model, column_name),
|
126
|
-
format: {},
|
127
|
-
reference: {
|
128
|
-
name: name,
|
129
|
-
model_name: ref.klass.name.underscore,
|
130
|
-
reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
|
131
|
-
foreign_key: ref.foreign_key,
|
132
|
-
polymorphic: ref.polymorphic? || is_attachment
|
133
|
-
},
|
134
|
-
virtual: false
|
135
|
-
}
|
121
|
+
build_reflection_column(name, model, ref, default_attrs)
|
136
122
|
end.compact
|
137
123
|
end
|
138
124
|
|
125
|
+
def build_reflection_column(name, model, ref, default_attrs)
|
126
|
+
column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
|
127
|
+
is_attachment = ref.klass.name == 'ActiveStorage::Attachment'
|
128
|
+
access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
|
129
|
+
|
130
|
+
{
|
131
|
+
name: column_name,
|
132
|
+
display_name: column_name.humanize,
|
133
|
+
column_type: is_attachment ? 'file' : 'integer',
|
134
|
+
access_type: access_type,
|
135
|
+
default_value: default_attrs[column_name],
|
136
|
+
validators: fetch_validators(model, column_name, ref),
|
137
|
+
format: {},
|
138
|
+
reference: {
|
139
|
+
name: name,
|
140
|
+
model_name: ref.klass.name.underscore,
|
141
|
+
reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
|
142
|
+
foreign_key: ref.foreign_key,
|
143
|
+
polymorphic: ref.polymorphic? || is_attachment
|
144
|
+
},
|
145
|
+
virtual: false
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
139
149
|
def fetch_associations(model)
|
140
150
|
model.reflections.map do |name, ref|
|
141
151
|
next if ref.has_one? || ref.belongs_to?
|
@@ -162,30 +172,43 @@ module Motor
|
|
162
172
|
end.compact
|
163
173
|
end
|
164
174
|
|
165
|
-
def fetch_validators(model, column_name)
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
{ includes: validator.send(:delimiter) }
|
170
|
-
when ActiveRecord::Validations::PresenceValidator
|
171
|
-
{ required: true }
|
172
|
-
when ActiveModel::Validations::FormatValidator
|
173
|
-
{ format: JsRegex.new(validator.options[:with]).to_h.slice(:source, :options) }
|
174
|
-
when ActiveRecord::Validations::LengthValidator
|
175
|
-
{ length: validator.options }
|
176
|
-
when ActiveModel::Validations::NumericalityValidator
|
177
|
-
{ numeric: validator.options }
|
175
|
+
def fetch_validators(model, column_name, reflection = nil)
|
176
|
+
validators =
|
177
|
+
if reflection&.belongs_to? && !reflection.options[:optional]
|
178
|
+
[{ required: true }]
|
178
179
|
else
|
179
|
-
|
180
|
+
[]
|
180
181
|
end
|
182
|
+
|
183
|
+
validators += model.validators_on(column_name).map do |validator|
|
184
|
+
build_validator_hash(validator)
|
181
185
|
end.compact
|
186
|
+
|
187
|
+
validators.uniq
|
188
|
+
end
|
189
|
+
|
190
|
+
def build_validator_hash(validator)
|
191
|
+
case validator
|
192
|
+
when ActiveModel::Validations::InclusionValidator
|
193
|
+
{ includes: validator.send(:delimiter) }
|
194
|
+
when ActiveRecord::Validations::PresenceValidator
|
195
|
+
{ required: true }
|
196
|
+
when ActiveModel::Validations::FormatValidator
|
197
|
+
{ format: JsRegex.new(validator.options[:with]).to_h.slice(:source, :options) }
|
198
|
+
when ActiveRecord::Validations::LengthValidator
|
199
|
+
{ length: validator.options }
|
200
|
+
when ActiveModel::Validations::NumericalityValidator
|
201
|
+
{ numeric: validator.options }
|
202
|
+
end
|
182
203
|
end
|
183
204
|
|
184
205
|
def eager_load_models!
|
185
|
-
|
186
|
-
Zeitwerk::Loader
|
187
|
-
|
188
|
-
|
206
|
+
MUTEX.synchronize do
|
207
|
+
if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
|
208
|
+
Zeitwerk::Loader.eager_load_all
|
209
|
+
else
|
210
|
+
Rails.application.eager_load!
|
211
|
+
end
|
189
212
|
end
|
190
213
|
end
|
191
214
|
end
|
@@ -27,40 +27,81 @@ module Motor
|
|
27
27
|
def merge_model(model, configs)
|
28
28
|
updated_model = model.merge(configs.slice(*RESOURCE_ATTRS))
|
29
29
|
|
30
|
-
updated_model
|
30
|
+
merge_actions!(updated_model, configs)
|
31
|
+
merge_assiciations!(updated_model, configs)
|
32
|
+
merge_columns!(updated_model, configs)
|
33
|
+
merge_tabs!(updated_model, configs)
|
34
|
+
merge_scopes!(updated_model, configs)
|
35
|
+
|
36
|
+
updated_model
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param model [HashWithIndifferentAccess]
|
40
|
+
# @param configs [HashWithIndifferentAccess]
|
41
|
+
# @return [HashWithIndifferentAccess]
|
42
|
+
def merge_assiciations!(model, configs)
|
43
|
+
model[:associations] = merge_by_name(
|
31
44
|
model[:associations],
|
32
45
|
configs[:associations],
|
33
46
|
{},
|
34
47
|
->(_) { true }
|
35
48
|
)
|
36
49
|
|
37
|
-
|
50
|
+
model
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param model [HashWithIndifferentAccess]
|
54
|
+
# @param configs [HashWithIndifferentAccess]
|
55
|
+
# @return [HashWithIndifferentAccess]
|
56
|
+
def merge_columns!(model, configs)
|
57
|
+
model[:columns] = merge_by_name(
|
38
58
|
model[:columns],
|
39
59
|
configs[:columns],
|
40
60
|
COLUMN_DEFAULTS,
|
41
61
|
->(scope) { !scope[:virtual] }
|
42
62
|
)
|
43
63
|
|
44
|
-
|
64
|
+
model
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param model [HashWithIndifferentAccess]
|
68
|
+
# @param configs [HashWithIndifferentAccess]
|
69
|
+
# @return [HashWithIndifferentAccess]
|
70
|
+
def merge_actions!(model, configs)
|
71
|
+
model[:actions] = merge_by_name(
|
45
72
|
model[:actions],
|
46
73
|
configs[:actions],
|
47
74
|
ACTION_DEFAULTS
|
48
75
|
)
|
49
76
|
|
50
|
-
|
77
|
+
model
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param model [HashWithIndifferentAccess]
|
81
|
+
# @param configs [HashWithIndifferentAccess]
|
82
|
+
# @return [HashWithIndifferentAccess]
|
83
|
+
def merge_tabs!(model, configs)
|
84
|
+
model[:tabs] = merge_by_name(
|
51
85
|
model[:tabs],
|
52
86
|
configs[:tabs],
|
53
87
|
TAB_DEFAULTS
|
54
88
|
)
|
55
89
|
|
56
|
-
|
90
|
+
model
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param model [HashWithIndifferentAccess]
|
94
|
+
# @param configs [HashWithIndifferentAccess]
|
95
|
+
# @return [HashWithIndifferentAccess]
|
96
|
+
def merge_scopes!(model, configs)
|
97
|
+
model[:scopes] = merge_by_name(
|
57
98
|
model[:scopes],
|
58
99
|
configs[:scopes],
|
59
100
|
SCOPE_DEFAULTS,
|
60
101
|
->(scope) { scope[:scope_type] != 'filter' }
|
61
102
|
)
|
62
103
|
|
63
|
-
|
104
|
+
model
|
64
105
|
end
|
65
106
|
|
66
107
|
# @param defaults [Array<HashWithIndifferentAccess>]
|
@@ -77,47 +77,26 @@ module Motor
|
|
77
77
|
normalized_preferences = existing_prefs.merge(normalized_preferences)
|
78
78
|
normalized_preferences = reject_default(default_prefs, normalized_preferences)
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
)
|
86
|
-
end
|
80
|
+
normalize_configs!(normalized_preferences, :columns, default_prefs, existing_prefs, new_prefs)
|
81
|
+
normalize_configs!(normalized_preferences, :associations, default_prefs, existing_prefs, new_prefs)
|
82
|
+
normalize_configs!(normalized_preferences, :actions, default_prefs, existing_prefs, new_prefs)
|
83
|
+
normalize_configs!(normalized_preferences, :tabs, default_prefs, existing_prefs, new_prefs)
|
84
|
+
normalize_configs!(normalized_preferences, :scopes, default_prefs, existing_prefs, new_prefs)
|
87
85
|
|
88
|
-
|
89
|
-
|
90
|
-
default_prefs[:associations],
|
91
|
-
existing_prefs.fetch(:associations, []),
|
92
|
-
new_prefs.fetch(:associations, [])
|
93
|
-
)
|
94
|
-
end
|
86
|
+
normalized_preferences.compact
|
87
|
+
end
|
95
88
|
|
96
|
-
|
97
|
-
|
98
|
-
default_prefs[:actions],
|
99
|
-
existing_prefs.fetch(:actions, []),
|
100
|
-
new_prefs.fetch(:actions, [])
|
101
|
-
)
|
102
|
-
end
|
89
|
+
def normalize_configs!(preferences, configs_name, default_prefs, existing_prefs, new_prefs)
|
90
|
+
return preferences if new_prefs[configs_name].blank?
|
103
91
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
new_prefs.fetch(:tabs, [])
|
109
|
-
)
|
110
|
-
end
|
92
|
+
normalized_configs = public_send("normalize_#{configs_name}",
|
93
|
+
default_prefs[configs_name],
|
94
|
+
existing_prefs.fetch(configs_name, []),
|
95
|
+
new_prefs.fetch(configs_name, []))
|
111
96
|
|
112
|
-
|
113
|
-
normalized_preferences[:scopes] = normalize_scopes(
|
114
|
-
default_prefs[:scopes],
|
115
|
-
existing_prefs.fetch(:scopes, []),
|
116
|
-
new_prefs.fetch(:scopes, [])
|
117
|
-
)
|
118
|
-
end
|
97
|
+
preferences[configs_name] = normalized_configs
|
119
98
|
|
120
|
-
|
99
|
+
preferences
|
121
100
|
end
|
122
101
|
|
123
102
|
# @param default_columns [Array<HashWithIndifferentAccess>]
|
@@ -23,21 +23,29 @@ module Motor
|
|
23
23
|
|
24
24
|
schema = sort_by_name(schema, configs['resources.order'])
|
25
25
|
|
26
|
-
schema.map
|
27
|
-
|
28
|
-
associations_order = configs["resources.#{model[:name]}.associations.order"]
|
29
|
-
actions_order = configs["resources.#{model[:name]}.actions.order"]
|
30
|
-
tabs_order = configs["resources.#{model[:name]}.tabs.order"]
|
31
|
-
scopes_order = configs["resources.#{model[:name]}.scopes.order"]
|
26
|
+
schema.map { |model| reorder_model(model, configs) }
|
27
|
+
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
)
|
40
|
-
|
29
|
+
def reorder_model(model, configs)
|
30
|
+
order_configs = build_order_configs(model[:name], configs)
|
31
|
+
|
32
|
+
model.merge(
|
33
|
+
columns: sort_by_name(sort_columns(model[:columns]), order_configs[:columns], sort_alphabetically: false),
|
34
|
+
associations: sort_by_name(model[:associations], order_configs[:associations]),
|
35
|
+
actions: sort_by_name(model[:actions], order_configs[:actions], sort_alphabetically: false),
|
36
|
+
tabs: sort_by_name(model[:tabs], order_configs[:tabs], sort_alphabetically: false),
|
37
|
+
scopes: sort_by_name(model[:scopes], order_configs[:scopes])
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_order_configs(model_name, configs)
|
42
|
+
{
|
43
|
+
columns: configs["resources.#{model_name}.columns.order"],
|
44
|
+
associations: configs["resources.#{model_name}.associations.order"],
|
45
|
+
actions: configs["resources.#{model_name}.actions.order"],
|
46
|
+
tabs: configs["resources.#{model_name}.tabs.order"],
|
47
|
+
scopes: configs["resources.#{model_name}.scopes.order"]
|
48
|
+
}
|
41
49
|
end
|
42
50
|
|
43
51
|
# @param list [Array<HashWithIndifferentAccess>]
|
data/lib/motor/queries.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Queries
|
5
|
+
module RenderSqlTemplate
|
6
|
+
SECTION_OPEN_REGEXP = /{{([#^])\s*(\w+)}}.*\z/m.freeze
|
7
|
+
VARIABLE_REGEXP = /{{\s*(\w+)\s*}}/m.freeze
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(sql, variables)
|
12
|
+
result = render_sections(sql, variables)
|
13
|
+
|
14
|
+
interpolate_variables(result, variables)
|
15
|
+
end
|
16
|
+
|
17
|
+
def interpolate_variables(sql, variables)
|
18
|
+
selected_variables = []
|
19
|
+
|
20
|
+
rendered =
|
21
|
+
sql.gsub(VARIABLE_REGEXP) do
|
22
|
+
variable_name = Regexp.last_match[1]
|
23
|
+
|
24
|
+
index = selected_variables.index { |name, _| name == variable_name }
|
25
|
+
selected_variables << [variable_name, variables[variable_name]] unless index
|
26
|
+
|
27
|
+
"$#{selected_variables.size}"
|
28
|
+
end
|
29
|
+
|
30
|
+
[rendered, selected_variables]
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_sections(sql, variables)
|
34
|
+
sql.sub(SECTION_OPEN_REGEXP) do |e|
|
35
|
+
variable_name = Regexp.last_match[2]
|
36
|
+
is_negative = Regexp.last_match[1] == '^'
|
37
|
+
|
38
|
+
_, content, rest = e.split(build_section_close_regexp(variable_name), 3)
|
39
|
+
|
40
|
+
is_present = variables[variable_name].present?
|
41
|
+
|
42
|
+
render_sections(is_present ^ is_negative ? content + rest.to_s : rest, variables)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_section_close_regexp(variable_name)
|
47
|
+
%r{{{[#^/]s*#{Regexp.escape(variable_name)}\s*}}}m
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -3,8 +3,7 @@
|
|
3
3
|
module Motor
|
4
4
|
module Queries
|
5
5
|
module RunQuery
|
6
|
-
DEFAULT_LIMIT =
|
7
|
-
INTERPOLATION_REGEXP = /{{(\w+)}}/.freeze
|
6
|
+
DEFAULT_LIMIT = 100_000
|
8
7
|
|
9
8
|
QueryResult = Struct.new(:data, :columns, keyword_init: true)
|
10
9
|
|
@@ -14,6 +13,10 @@ module Motor
|
|
14
13
|
|
15
14
|
module_function
|
16
15
|
|
16
|
+
# @param query [Motor::Query]
|
17
|
+
# @param variables_hash [Hash]
|
18
|
+
# @param limit [Integer]
|
19
|
+
# @return [Motor::Queries::RunQuery::QueryResult]
|
17
20
|
def call(query, variables_hash: nil, limit: DEFAULT_LIMIT)
|
18
21
|
variables_hash ||= {}
|
19
22
|
|
@@ -22,15 +25,27 @@ module Motor
|
|
22
25
|
QueryResult.new(data: result.rows, columns: build_columns_hash(result))
|
23
26
|
end
|
24
27
|
|
28
|
+
# @param query [Motor::Query]
|
29
|
+
# @param limit [Integer]
|
30
|
+
# @param variables_hash [Hash]
|
31
|
+
# @return [ActiveRecord::Result]
|
25
32
|
def execute_query(query, limit, variables_hash)
|
33
|
+
result = nil
|
26
34
|
statement = prepare_sql_statement(query, limit, variables_hash)
|
27
35
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
ActiveRecord::Base.transaction do
|
37
|
+
result =
|
38
|
+
case ActiveRecord::Base.connection
|
39
|
+
when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
40
|
+
PostgresqlExecQuery.call(ActiveRecord::Base.connection, statement)
|
41
|
+
else
|
42
|
+
ActiveRecord::Base.connection.exec_query(*statement)
|
43
|
+
end
|
44
|
+
|
45
|
+
raise ActiveRecord::Rollback
|
33
46
|
end
|
47
|
+
|
48
|
+
result
|
34
49
|
end
|
35
50
|
|
36
51
|
# @param result [ActiveRecord::Result]
|
@@ -47,27 +62,40 @@ module Motor
|
|
47
62
|
end
|
48
63
|
end
|
49
64
|
|
65
|
+
# @param query [Motor::Query]
|
66
|
+
# @param limit [Integer]
|
67
|
+
# @param variables_hash [Hash]
|
68
|
+
# @return [Array]
|
50
69
|
def prepare_sql_statement(query, limit, variables_hash)
|
51
|
-
variables = query.preferences.fetch(:variables, [])
|
52
|
-
|
53
|
-
sql =
|
54
|
-
query.sql_body.gsub(INTERPOLATION_REGEXP) do
|
55
|
-
index = variables.index { |name, _| name == (Regexp.last_match[1]) } + 1
|
70
|
+
variables = merge_variable_default_values(query.preferences.fetch(:variables, []), variables_hash)
|
56
71
|
|
57
|
-
|
58
|
-
end
|
72
|
+
sql, query_variables = RenderSqlTemplate.call(query.sql_body, variables)
|
59
73
|
|
60
|
-
attributes =
|
61
|
-
variables.map do |variable_name, default_value|
|
62
|
-
ActiveRecord::Relation::QueryAttribute.new(
|
63
|
-
variable_name,
|
64
|
-
variables_hash[variable_name] || default_value,
|
65
|
-
ActiveRecord::Type::Value.new
|
66
|
-
)
|
67
|
-
end
|
74
|
+
attributes = build_statement_attributes(query_variables)
|
68
75
|
|
69
76
|
[format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.gsub(/;\z/, ''), limit: limit), 'SQL', attributes]
|
70
77
|
end
|
78
|
+
|
79
|
+
# @param variables [Array<(String, Object)>]
|
80
|
+
# @return [Array<ActiveRecord::Relation::QueryAttribute>]
|
81
|
+
def build_statement_attributes(variables)
|
82
|
+
variables.map do |variable_name, value|
|
83
|
+
ActiveRecord::Relation::QueryAttribute.new(
|
84
|
+
variable_name,
|
85
|
+
value,
|
86
|
+
ActiveRecord::Type::Value.new
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param variable_configs [Array<Hash>]
|
92
|
+
# @param variable_hash [Hash]
|
93
|
+
# @return [Hash]
|
94
|
+
def merge_variable_default_values(variable_configs, variables_hash)
|
95
|
+
variable_configs.each_with_object({}) do |variable, acc|
|
96
|
+
acc[variable[:name]] = variables_hash[variable[:name]] || variable[:default_value]
|
97
|
+
end
|
98
|
+
end
|
71
99
|
end
|
72
100
|
end
|
73
101
|
end
|
data/lib/motor/ui_configs.rb
CHANGED
@@ -27,22 +27,42 @@ module Motor
|
|
27
27
|
{
|
28
28
|
base_path: Motor::Admin.routes.url_helpers.motor_path,
|
29
29
|
schema: Motor::BuildSchema.call,
|
30
|
-
header_links:
|
31
|
-
queries:
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
.as_json(only: %i[id title updated_at],
|
36
|
-
include: { tags: { only: %i[id name] } }),
|
37
|
-
alerts: Motor::Alert.all.active.preload(:tags)
|
38
|
-
.as_json(only: %i[id name is_enabled updated_at],
|
39
|
-
include: { tags: { only: %i[id name] } }),
|
40
|
-
forms: Motor::Form.all.active.preload(:tags)
|
41
|
-
.as_json(only: %i[id name updated_at],
|
42
|
-
include: { tags: { only: %i[id name] } })
|
30
|
+
header_links: header_links_data_hash,
|
31
|
+
queries: queries_data_hash,
|
32
|
+
dashboards: dashboards_data_hash,
|
33
|
+
alerts: alerts_data_hash,
|
34
|
+
forms: forms_data_hash
|
43
35
|
}
|
44
36
|
end
|
45
37
|
|
38
|
+
def header_links_data_hash
|
39
|
+
Motor::Config.find_by(key: 'header.links')&.value || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def queries_data_hash
|
43
|
+
Motor::Query.all.active.preload(:tags)
|
44
|
+
.as_json(only: %i[id name updated_at],
|
45
|
+
include: { tags: { only: %i[id name] } })
|
46
|
+
end
|
47
|
+
|
48
|
+
def dashboards_data_hash
|
49
|
+
Motor::Dashboard.all.active.preload(:tags)
|
50
|
+
.as_json(only: %i[id title updated_at],
|
51
|
+
include: { tags: { only: %i[id name] } })
|
52
|
+
end
|
53
|
+
|
54
|
+
def alerts_data_hash
|
55
|
+
Motor::Alert.all.active.preload(:tags)
|
56
|
+
.as_json(only: %i[id name is_enabled updated_at],
|
57
|
+
include: { tags: { only: %i[id name] } })
|
58
|
+
end
|
59
|
+
|
60
|
+
def forms_data_hash
|
61
|
+
Motor::Form.all.active.preload(:tags)
|
62
|
+
.as_json(only: %i[id name updated_at],
|
63
|
+
include: { tags: { only: %i[id name] } })
|
64
|
+
end
|
65
|
+
|
46
66
|
# @return [String]
|
47
67
|
def cache_key
|
48
68
|
ActiveRecord::Base.connection.execute(
|
data/lib/motor/version.rb
CHANGED
Binary file
|
Binary file
|
data/ui/dist/manifest.json
CHANGED
@@ -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-
|
9
|
-
"main-
|
10
|
-
"main-
|
11
|
-
"main.css": "main-
|
12
|
-
"main.js": "main-
|
8
|
+
"main-b27ee7bbbc5e97fb1743.css.gz": "main-b27ee7bbbc5e97fb1743.css.gz",
|
9
|
+
"main-b27ee7bbbc5e97fb1743.js.LICENSE.txt": "main-b27ee7bbbc5e97fb1743.js.LICENSE.txt",
|
10
|
+
"main-b27ee7bbbc5e97fb1743.js.gz": "main-b27ee7bbbc5e97fb1743.js.gz",
|
11
|
+
"main.css": "main-b27ee7bbbc5e97fb1743.css",
|
12
|
+
"main.js": "main-b27ee7bbbc5e97fb1743.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.
|
4
|
+
version: 0.1.31
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pete Matsyburka
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-05-
|
11
|
+
date: 2021-05-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord-filter
|
@@ -132,6 +132,7 @@ files:
|
|
132
132
|
- LICENSE
|
133
133
|
- README.md
|
134
134
|
- Rakefile
|
135
|
+
- app/controllers/concerns/motor/wrap_io_params.rb
|
135
136
|
- app/controllers/motor/active_storage_attachments_controller.rb
|
136
137
|
- app/controllers/motor/alerts_controller.rb
|
137
138
|
- app/controllers/motor/api_base_controller.rb
|
@@ -182,7 +183,6 @@ files:
|
|
182
183
|
- lib/motor/alerts/persistance.rb
|
183
184
|
- lib/motor/alerts/scheduled_alerts_cache.rb
|
184
185
|
- lib/motor/alerts/scheduler.rb
|
185
|
-
- lib/motor/api.rb
|
186
186
|
- lib/motor/api_query.rb
|
187
187
|
- lib/motor/api_query/apply_scope.rb
|
188
188
|
- lib/motor/api_query/build_json.rb
|
@@ -208,13 +208,14 @@ files:
|
|
208
208
|
- lib/motor/queries.rb
|
209
209
|
- lib/motor/queries/persistance.rb
|
210
210
|
- lib/motor/queries/postgresql_exec_query.rb
|
211
|
+
- lib/motor/queries/render_sql_template.rb
|
211
212
|
- lib/motor/queries/run_query.rb
|
212
213
|
- lib/motor/tags.rb
|
213
214
|
- lib/motor/ui_configs.rb
|
214
215
|
- lib/motor/version.rb
|
215
216
|
- ui/dist/fonts/ionicons.woff2
|
216
|
-
- ui/dist/main-
|
217
|
-
- ui/dist/main-
|
217
|
+
- ui/dist/main-b27ee7bbbc5e97fb1743.css.gz
|
218
|
+
- ui/dist/main-b27ee7bbbc5e97fb1743.js.gz
|
218
219
|
- ui/dist/manifest.json
|
219
220
|
homepage:
|
220
221
|
licenses:
|
data/lib/motor/api.rb
DELETED
Binary file
|