motor-admin 0.1.22 → 0.1.29
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/lib/motor/admin.rb +15 -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 -48
- data/lib/motor/build_schema/merge_schema_configs.rb +48 -7
- data/lib/motor/build_schema/persist_resource_configs.rb +17 -37
- 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-25b3270c536d4f5d15b5.css.gz → main-482137e0bc1ba9f875dc.css.gz} +0 -0
- data/ui/dist/main-482137e0bc1ba9f875dc.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +6 -4
- data/ui/dist/main-25b3270c536d4f5d15b5.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: 2f18c5fae6ca2c70b739f68d0a9b45f456b6c9f9592b4c44d216210a067b2e33
|
4
|
+
data.tar.gz: 4a65316ecce576b9de027fe93ff14bf9307826269af771fbfc9a7bbf3699f051
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f51029e47fe84f67f34ced365523f2214c44986f241ad35a77405f067a556bbca8ca124cb09812b007a709101729ae3dfc35aa94284c46dbbf2f63a990459001
|
7
|
+
data.tar.gz: 555b5570c50258d3ee00a317795868cee3281e0d3675d894e25773f26977d21d40781b031a4e3835469fa580d39b3e1ba5b87f11dcad83986b0bd144f612bc5c
|
@@ -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
|
data/lib/motor/admin.rb
CHANGED
@@ -14,6 +14,21 @@ module Motor
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
initializer 'motor.basic_auth' do
|
18
|
+
next if ENV['MOTOR_AUTH_PASSWORD'].blank?
|
19
|
+
|
20
|
+
config.middleware.use Rack::Auth::Basic do |username, password|
|
21
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
22
|
+
::Digest::SHA256.hexdigest(username),
|
23
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_USERNAME'].to_s)
|
24
|
+
) &
|
25
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
26
|
+
::Digest::SHA256.hexdigest(password),
|
27
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_PASSWORD'].to_s)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
17
32
|
initializer 'motor.active_storage.extensions' do
|
18
33
|
ActiveSupport.on_load(:active_storage_attachment) do
|
19
34
|
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,21 +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
|
-
virtual: false
|
94
|
-
}
|
87
|
+
build_table_column(column, model, default_attrs)
|
95
88
|
end.compact
|
96
89
|
|
97
90
|
reference_columns + table_columns
|
98
91
|
end
|
99
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
|
+
|
100
107
|
def fetch_reference_columns(model)
|
101
108
|
default_attrs = model.new.attributes
|
102
109
|
|
@@ -109,31 +116,36 @@ module Motor
|
|
109
116
|
next
|
110
117
|
end
|
111
118
|
|
112
|
-
column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
|
113
|
-
|
114
119
|
next if ref.klass.name == 'ActiveStorage::Blob'
|
115
120
|
|
116
|
-
|
117
|
-
|
118
|
-
{
|
119
|
-
name: column_name,
|
120
|
-
display_name: column_name.humanize,
|
121
|
-
column_type: is_attachment ? 'file' : 'integer',
|
122
|
-
access_type: ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY,
|
123
|
-
default_value: default_attrs[column_name],
|
124
|
-
validators: fetch_validators(model, column_name),
|
125
|
-
reference: {
|
126
|
-
name: name,
|
127
|
-
model_name: ref.klass.name.underscore,
|
128
|
-
reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
|
129
|
-
foreign_key: ref.foreign_key,
|
130
|
-
polymorphic: ref.polymorphic? || is_attachment
|
131
|
-
},
|
132
|
-
virtual: false
|
133
|
-
}
|
121
|
+
build_reflection_column(name, model, ref, default_attrs)
|
134
122
|
end.compact
|
135
123
|
end
|
136
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
|
+
|
137
149
|
def fetch_associations(model)
|
138
150
|
model.reflections.map do |name, ref|
|
139
151
|
next if ref.has_one? || ref.belongs_to?
|
@@ -160,30 +172,43 @@ module Motor
|
|
160
172
|
end.compact
|
161
173
|
end
|
162
174
|
|
163
|
-
def fetch_validators(model, column_name)
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
{ includes: validator.send(:delimiter) }
|
168
|
-
when ActiveRecord::Validations::PresenceValidator
|
169
|
-
{ required: true }
|
170
|
-
when ActiveModel::Validations::FormatValidator
|
171
|
-
{ format: JsRegex.new(validator.options[:with]).to_h.slice(:source, :options) }
|
172
|
-
when ActiveRecord::Validations::LengthValidator
|
173
|
-
{ length: validator.options }
|
174
|
-
when ActiveModel::Validations::NumericalityValidator
|
175
|
-
{ 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 }]
|
176
179
|
else
|
177
|
-
|
180
|
+
[]
|
178
181
|
end
|
182
|
+
|
183
|
+
validators += model.validators_on(column_name).map do |validator|
|
184
|
+
build_validator_hash(validator)
|
179
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
|
180
203
|
end
|
181
204
|
|
182
205
|
def eager_load_models!
|
183
|
-
|
184
|
-
Zeitwerk::Loader
|
185
|
-
|
186
|
-
|
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
|
187
212
|
end
|
188
213
|
end
|
189
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>]
|
@@ -4,7 +4,7 @@ module Motor
|
|
4
4
|
module BuildSchema
|
5
5
|
module PersistResourceConfigs
|
6
6
|
RESOURCE_ATTRS = %w[display_name visible].freeze
|
7
|
-
COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual].freeze
|
7
|
+
COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual format].freeze
|
8
8
|
ASSOCIATION_ATTRS = %w[name display_name visible].freeze
|
9
9
|
SCOPE_ATTRS = %w[name display_name scope_type preferences visible].freeze
|
10
10
|
ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
|
@@ -14,6 +14,7 @@ module Motor
|
|
14
14
|
access_type: 'read_write',
|
15
15
|
default_value: nil,
|
16
16
|
reference: nil,
|
17
|
+
format: {},
|
17
18
|
validators: []
|
18
19
|
}.with_indifferent_access
|
19
20
|
|
@@ -76,47 +77,26 @@ module Motor
|
|
76
77
|
normalized_preferences = existing_prefs.merge(normalized_preferences)
|
77
78
|
normalized_preferences = reject_default(default_prefs, normalized_preferences)
|
78
79
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
)
|
85
|
-
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)
|
86
85
|
|
87
|
-
|
88
|
-
|
89
|
-
default_prefs[:associations],
|
90
|
-
existing_prefs.fetch(:associations, []),
|
91
|
-
new_prefs.fetch(:associations, [])
|
92
|
-
)
|
93
|
-
end
|
86
|
+
normalized_preferences.compact
|
87
|
+
end
|
94
88
|
|
95
|
-
|
96
|
-
|
97
|
-
default_prefs[:actions],
|
98
|
-
existing_prefs.fetch(:actions, []),
|
99
|
-
new_prefs.fetch(:actions, [])
|
100
|
-
)
|
101
|
-
end
|
89
|
+
def normalize_configs!(preferences, configs_name, default_prefs, existing_prefs, new_prefs)
|
90
|
+
return preferences if new_prefs[configs_name].blank?
|
102
91
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
new_prefs.fetch(:tabs, [])
|
108
|
-
)
|
109
|
-
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, []))
|
110
96
|
|
111
|
-
|
112
|
-
normalized_preferences[:scopes] = normalize_scopes(
|
113
|
-
default_prefs[:scopes],
|
114
|
-
existing_prefs.fetch(:scopes, []),
|
115
|
-
new_prefs.fetch(:scopes, [])
|
116
|
-
)
|
117
|
-
end
|
97
|
+
preferences[configs_name] = normalized_configs
|
118
98
|
|
119
|
-
|
99
|
+
preferences
|
120
100
|
end
|
121
101
|
|
122
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-482137e0bc1ba9f875dc.css.gz": "main-482137e0bc1ba9f875dc.css.gz",
|
9
|
+
"main-482137e0bc1ba9f875dc.js.LICENSE.txt": "main-482137e0bc1ba9f875dc.js.LICENSE.txt",
|
10
|
+
"main-482137e0bc1ba9f875dc.js.gz": "main-482137e0bc1ba9f875dc.js.gz",
|
11
|
+
"main.css": "main-482137e0bc1ba9f875dc.css",
|
12
|
+
"main.js": "main-482137e0bc1ba9f875dc.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.29
|
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-07 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
|
@@ -208,13 +209,14 @@ files:
|
|
208
209
|
- lib/motor/queries.rb
|
209
210
|
- lib/motor/queries/persistance.rb
|
210
211
|
- lib/motor/queries/postgresql_exec_query.rb
|
212
|
+
- lib/motor/queries/render_sql_template.rb
|
211
213
|
- lib/motor/queries/run_query.rb
|
212
214
|
- lib/motor/tags.rb
|
213
215
|
- lib/motor/ui_configs.rb
|
214
216
|
- lib/motor/version.rb
|
215
217
|
- ui/dist/fonts/ionicons.woff2
|
216
|
-
- ui/dist/main-
|
217
|
-
- ui/dist/main-
|
218
|
+
- ui/dist/main-482137e0bc1ba9f875dc.css.gz
|
219
|
+
- ui/dist/main-482137e0bc1ba9f875dc.js.gz
|
218
220
|
- ui/dist/manifest.json
|
219
221
|
homepage:
|
220
222
|
licenses:
|
Binary file
|