motor-admin 0.1.21 → 0.1.28

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6b8a4bcbd17fc0eed3b91dfac13285623adbfeda90a20a414193384070cccff
4
- data.tar.gz: 303af4bb7f49abfd02dfe91c6aa03278e1d6572e1831e73cd60fcef0b2a96a29
3
+ metadata.gz: 5799ce999fa943638fa7c4d655400c1418ee80977bf47f64a71e6d5c496d02de
4
+ data.tar.gz: da99ec7b1c48708d983b663a7f9cb79b6d0e270b2fc9f5538a8121cfabdbc209
5
5
  SHA512:
6
- metadata.gz: 144c32f7aa7902c4b2200fdde19b7487d1476856380c79b1d694d164227f98529b204a81f4bffcfc281a0951be13f585a9fe5ae57014bb66b38b94e49199b44b
7
- data.tar.gz: 78b1501b59c676ecc389febca43eadc961514e3720fcc3256c6664086f60f5c0167973c7b19c7ce1a8705404b2111136872a683a451bd9633b21531bf59a06e0
6
+ metadata.gz: c5fc31a79b01ad0e716700a7c3daed980b293d13081294d1013918cce6f4d24cf4c104e0c44144cde225fd3b1ce07253f9b3ddaf44f1eca2252a01ff9124c52e
7
+ data.tar.gz: efca3a6d85ce6e33c83a63f3cd5addbd75b2baf099bd96c5efbac40445520a7725d68a5c3d0564792f06bdf12fed8958b13a6189047303d3c77f7f0af0ff0bd4
@@ -7,7 +7,7 @@ module Motor
7
7
  load_and_authorize_resource :attachment, class: 'ActiveStorage::Attachment', parent: false
8
8
 
9
9
  def create
10
- if @attachment.record.respond_to?("#{@attachment.name}_attachment=") || @attachment.record.respond_to?("#{@attachment.name}_attachments=")
10
+ if attachable?(@attachment.record)
11
11
  @attachment.record.public_send(@attachment.name).attach(
12
12
  io: StringIO.new(params.dig(:data, :file, :io).to_s.encode('ISO-8859-1')),
13
13
  filename: params.dig(:data, :file, :filename)
@@ -21,6 +21,11 @@ module Motor
21
21
 
22
22
  private
23
23
 
24
+ def attachable?(record)
25
+ record.respond_to?("#{@attachment.name}_attachment=") ||
26
+ record.respond_to?("#{@attachment.name}_attachments=")
27
+ end
28
+
24
29
  def attachment_params
25
30
  params.require(:data).except(:file).permit!
26
31
  end
@@ -2,7 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>Motor Admin</title>
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
6
6
  <link rel="stylesheet" media="all" href="<%= Motor::Assets.asset_path('main.css') %>">
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
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
- acc[part] = {}
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
- if key == model_name || model_name.split('/').last == key
54
- json_params.merge!(build_fields_hash(model, fields))
55
- else
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
- fields_hash = build_fields_hash(model.reflections[key]&.klass, fields)
58
+ def merge_fields_params!(key, fields, json_params, model)
59
+ model_name = model.name.underscore
59
60
 
60
- hash.merge!(fields_hash)
61
- end
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
 
@@ -22,7 +22,11 @@ module Motor
22
22
  *path, _ = key.split('.')
23
23
 
24
24
  path.reduce(result) do |acc, fragment|
25
- acc[fragment] = {}
25
+ hash = {}
26
+
27
+ acc[fragment] = hash
28
+
29
+ hash
26
30
  end
27
31
  end
28
32
  end
@@ -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
- is_attachment = ref.klass.name == 'ActiveStorage::Attachment'
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),
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?
@@ -180,10 +192,12 @@ module Motor
180
192
  end
181
193
 
182
194
  def eager_load_models!
183
- if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
184
- Zeitwerk::Loader.eager_load_all
185
- else
186
- Rails.application.eager_load!
195
+ MUTEX.synchronize do
196
+ if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
197
+ Zeitwerk::Loader.eager_load_all
198
+ else
199
+ Rails.application.eager_load!
200
+ end
187
201
  end
188
202
  end
189
203
  end
@@ -27,50 +27,97 @@ module Motor
27
27
  def merge_model(model, configs)
28
28
  updated_model = model.merge(configs.slice(*RESOURCE_ATTRS))
29
29
 
30
- updated_model[:associations] = merge_by_name(
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
- configs[:associations]
45
+ configs[:associations],
46
+ {},
47
+ ->(_) { true }
33
48
  )
34
49
 
35
- updated_model[:columns] = merge_by_name(
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(
36
58
  model[:columns],
37
59
  configs[:columns],
38
- COLUMN_DEFAULTS
60
+ COLUMN_DEFAULTS,
61
+ ->(scope) { !scope[:virtual] }
39
62
  )
40
63
 
41
- updated_model[:actions] = merge_by_name(
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(
42
72
  model[:actions],
43
73
  configs[:actions],
44
74
  ACTION_DEFAULTS
45
75
  )
46
76
 
47
- updated_model[:tabs] = merge_by_name(
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(
48
85
  model[:tabs],
49
86
  configs[:tabs],
50
87
  TAB_DEFAULTS
51
88
  )
52
89
 
53
- updated_model[:scopes] = merge_by_name(
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(
54
98
  model[:scopes],
55
99
  configs[:scopes],
56
- SCOPE_DEFAULTS
100
+ SCOPE_DEFAULTS,
101
+ ->(scope) { scope[:scope_type] != 'filter' }
57
102
  )
58
103
 
59
- updated_model
104
+ model
60
105
  end
61
106
 
62
107
  # @param defaults [Array<HashWithIndifferentAccess>]
63
108
  # @param configs [Array<HashWithIndifferentAccess>]
64
109
  # @return [Array<HashWithIndifferentAccess>]
65
- def merge_by_name(defaults, configs, default_attrs = {})
110
+ def merge_by_name(defaults, configs, default_attrs = {}, default_item_check = nil)
66
111
  return defaults if configs.blank?
67
112
 
68
113
  (defaults.pluck(:name) + configs.pluck(:name)).uniq.map do |name|
69
- config_item = configs.find { |e| e[:name] == name } || {}
70
- default_item = defaults.find { |e| e[:name] == name } || default_attrs
114
+ config_item = configs.find { |e| e[:name] == name }
115
+ default_item = defaults.find { |e| e[:name] == name }
71
116
 
72
- default_item.merge(config_item)
73
- end
117
+ next if default_item.nil? && default_item_check&.call(config_item)
118
+
119
+ (default_item || default_attrs).merge(config_item || {})
120
+ end.compact
74
121
  end
75
122
 
76
123
  # @return [HashWithIndifferentAccess<String, 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
 
@@ -24,6 +25,7 @@ module Motor
24
25
 
25
26
  TAB_DEFAULTS = {
26
27
  visible: true,
28
+ tab_type: 'default',
27
29
  preferences: {}
28
30
  }.with_indifferent_access
29
31
 
@@ -75,47 +77,26 @@ module Motor
75
77
  normalized_preferences = existing_prefs.merge(normalized_preferences)
76
78
  normalized_preferences = reject_default(default_prefs, normalized_preferences)
77
79
 
78
- if new_prefs[:columns].present?
79
- normalized_preferences[:columns] = normalize_columns(
80
- default_prefs[:columns],
81
- existing_prefs.fetch(:columns, []),
82
- new_prefs.fetch(:columns, [])
83
- )
84
- 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)
85
85
 
86
- if new_prefs[:associations].present?
87
- normalized_preferences[:associations] = normalize_associations(
88
- default_prefs[:associations],
89
- existing_prefs.fetch(:associations, []),
90
- new_prefs.fetch(:associations, [])
91
- )
92
- end
86
+ normalized_preferences.compact
87
+ end
93
88
 
94
- if new_prefs[:actions].present?
95
- normalized_preferences[:actions] = normalize_actions(
96
- default_prefs[:actions],
97
- existing_prefs.fetch(:actions, []),
98
- new_prefs.fetch(:actions, [])
99
- )
100
- end
89
+ def normalize_configs!(preferences, configs_name, default_prefs, existing_prefs, new_prefs)
90
+ return preferences if new_prefs[configs_name].blank?
101
91
 
102
- if new_prefs[:tabs].present?
103
- normalized_preferences[:tabs] = normalize_tabs(
104
- default_prefs[:tabs],
105
- existing_prefs.fetch(:tabs, []),
106
- new_prefs.fetch(:tabs, [])
107
- )
108
- end
92
+ normalized_configs = public_send("normalize_#{configs_name}",
93
+ default_prefs[:actions],
94
+ existing_prefs.fetch(:actions, []),
95
+ new_prefs.fetch(:actions, []))
109
96
 
110
- if new_prefs[:scopes].present?
111
- normalized_preferences[:scopes] = normalize_scopes(
112
- default_prefs[:scopes],
113
- existing_prefs.fetch(:scopes, []),
114
- new_prefs.fetch(:scopes, [])
115
- )
116
- end
97
+ preferences[configs_name] = normalized_configs
117
98
 
118
- normalized_preferences.compact
99
+ preferences
119
100
  end
120
101
 
121
102
  # @param default_columns [Array<HashWithIndifferentAccess>]
@@ -123,7 +104,7 @@ module Motor
123
104
  # @param new_columns [Array<HashWithIndifferentAccess>]
124
105
  # @return [Array<HashWithIndifferentAccess>]
125
106
  def normalize_columns(default_columns, existing_columns, new_columns)
126
- (existing_columns.pluck(:name) + new_columns.pluck(:name)).uniq.map do |name|
107
+ fetch_update_names(existing_columns, new_columns).uniq.map do |name|
127
108
  new_column = safe_fetch_by_name(new_columns, name)
128
109
 
129
110
  next if new_column[:_remove]
@@ -135,7 +116,11 @@ module Motor
135
116
  normalized_column = existing_column.merge(column_attrs)
136
117
  normalized_column = reject_default(default_column, normalized_column)
137
118
 
138
- normalized_column.merge(name: name) if normalized_column.present?
119
+ next if normalized_column.blank?
120
+
121
+ normalized_column[:name] ||= name
122
+
123
+ normalized_column
139
124
  end.compact.presence
140
125
  end
141
126
 
@@ -144,7 +129,7 @@ module Motor
144
129
  # @param new_actions [Array<HashWithIndifferentAccess>]
145
130
  # @return [Array<HashWithIndifferentAccess>]
146
131
  def normalize_actions(default_actions, existing_actions, new_actions)
147
- (existing_actions.pluck(:name) + new_actions.pluck(:name)).uniq.map do |name|
132
+ fetch_update_names(existing_actions, new_actions).map do |name|
148
133
  new_action = safe_fetch_by_name(new_actions, name)
149
134
 
150
135
  next if new_action[:_remove]
@@ -156,7 +141,11 @@ module Motor
156
141
  normalized_action = existing_action.merge(action_attrs)
157
142
  normalized_action = reject_default(default_action.presence || ACTION_DEFAULTS, normalized_action)
158
143
 
159
- normalized_action.merge(name: name) if normalized_action.present?
144
+ next if normalized_action.blank?
145
+
146
+ normalized_action[:name] ||= name
147
+
148
+ normalized_action
160
149
  end.compact.presence
161
150
  end
162
151
 
@@ -165,7 +154,7 @@ module Motor
165
154
  # @param new_tabs [Array<HashWithIndifferentAccess>]
166
155
  # @return [Array<HashWithIndifferentAccess>]
167
156
  def normalize_tabs(default_tabs, existing_tabs, new_tabs)
168
- (existing_tabs.pluck(:name) + new_tabs.pluck(:name)).uniq.map do |name|
157
+ fetch_update_names(existing_tabs, new_tabs).uniq.map do |name|
169
158
  new_tab = safe_fetch_by_name(new_tabs, name)
170
159
 
171
160
  next if new_tab[:_remove]
@@ -177,7 +166,11 @@ module Motor
177
166
  normalized_tab = existing_tab.merge(tab_attrs)
178
167
  normalized_tab = reject_default(default_tab.presence || TAB_DEFAULTS, normalized_tab)
179
168
 
180
- normalized_tab.merge(name: name) if normalized_tab.present?
169
+ next if normalized_tab.blank?
170
+
171
+ normalized_tab[:name] ||= name
172
+
173
+ normalized_tab
181
174
  end.compact.presence
182
175
  end
183
176
 
@@ -186,7 +179,7 @@ module Motor
186
179
  # @param new_scopes [Array<HashWithIndifferentAccess>]
187
180
  # @return [Array<HashWithIndifferentAccess>]
188
181
  def normalize_scopes(default_scopes, existing_scopes, new_scopes)
189
- (existing_scopes.pluck(:name) + new_scopes.pluck(:name)).uniq.map do |name|
182
+ fetch_update_names(existing_scopes, new_scopes).uniq.map do |name|
190
183
  new_scope = safe_fetch_by_name(new_scopes, name)
191
184
 
192
185
  next if new_scope[:_remove]
@@ -198,7 +191,11 @@ module Motor
198
191
  normalized_scope = existing_scope.merge(scope_attrs)
199
192
  normalized_scope = reject_default(default_scope.presence || SCOPE_DEFAULTS, normalized_scope)
200
193
 
201
- normalized_scope.merge(name: name) if normalized_scope.present?
194
+ next if normalized_scope.blank?
195
+
196
+ normalized_scope[:name] ||= name
197
+
198
+ normalized_scope
202
199
  end.compact.presence
203
200
  end
204
201
 
@@ -220,8 +217,14 @@ module Motor
220
217
  end.compact.presence
221
218
  end
222
219
 
220
+ def fetch_update_names(existing_items, new_items)
221
+ new_names = new_items.map { |e| e[:_update] || e[:name] }
222
+
223
+ (existing_items.pluck(:name) + new_names).uniq
224
+ end
225
+
223
226
  def safe_fetch_by_name(list, name)
224
- list.find { |e| e[:name] == name } || {}
227
+ list.find { |e| e[:_update] == name || e[:name] == name } || {}
225
228
  end
226
229
 
227
230
  # @param resource_name [String]
@@ -23,21 +23,29 @@ module Motor
23
23
 
24
24
  schema = sort_by_name(schema, configs['resources.order'])
25
25
 
26
- schema.map do |model|
27
- columns_order = configs["resources.#{model[:name]}.columns.order"]
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
- model.merge(
34
- columns: sort_by_name(sort_columns(model[:columns]), columns_order, sort_alphabetically: false),
35
- associations: sort_by_name(model[:associations], associations_order),
36
- actions: sort_by_name(model[:actions], actions_order, sort_alphabetically: false),
37
- tabs: sort_by_name(model[:tabs], tabs_order, sort_alphabetically: false),
38
- scopes: sort_by_name(model[:scopes], scopes_order)
39
- )
40
- end
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
@@ -5,6 +5,7 @@ module Motor
5
5
  end
6
6
  end
7
7
 
8
+ require_relative './queries/render_sql_template'
8
9
  require_relative './queries/run_query'
9
10
  require_relative './queries/persistance'
10
11
  require_relative './queries/postgresql_exec_query'
@@ -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 = 1_000_000
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
- case ActiveRecord::Base.connection
29
- when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
30
- PostgresqlExecQuery.call(ActiveRecord::Base.connection, statement)
31
- else
32
- ActiveRecord::Base.connection.exec_query(*statement)
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, []).pluck(:name, :default_value)
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
- "$#{index}"
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
@@ -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: Motor::Config.find_by(key: 'header.links')&.value || [],
31
- queries: Motor::Query.all.active.preload(:tags)
32
- .as_json(only: %i[id name updated_at],
33
- include: { tags: { only: %i[id name] } }),
34
- dashboards: Motor::Dashboard.all.active.preload(:tags)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- VERSION = '0.1.21'
4
+ VERSION = '0.1.28'
5
5
  end
@@ -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-015cc9721345d5b8af80.css.gz": "main-015cc9721345d5b8af80.css.gz",
9
- "main-015cc9721345d5b8af80.js.LICENSE.txt": "main-015cc9721345d5b8af80.js.LICENSE.txt",
10
- "main-015cc9721345d5b8af80.js.gz": "main-015cc9721345d5b8af80.js.gz",
11
- "main.css": "main-015cc9721345d5b8af80.css",
12
- "main.js": "main-015cc9721345d5b8af80.js"
8
+ "main-4da1a5102d7bc66aefd0.css.gz": "main-4da1a5102d7bc66aefd0.css.gz",
9
+ "main-4da1a5102d7bc66aefd0.js.LICENSE.txt": "main-4da1a5102d7bc66aefd0.js.LICENSE.txt",
10
+ "main-4da1a5102d7bc66aefd0.js.gz": "main-4da1a5102d7bc66aefd0.js.gz",
11
+ "main.css": "main-4da1a5102d7bc66aefd0.css",
12
+ "main.js": "main-4da1a5102d7bc66aefd0.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.21
4
+ version: 0.1.28
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-02 00:00:00.000000000 Z
11
+ date: 2021-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -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-015cc9721345d5b8af80.css.gz
217
- - ui/dist/main-015cc9721345d5b8af80.js.gz
217
+ - ui/dist/main-4da1a5102d7bc66aefd0.css.gz
218
+ - ui/dist/main-4da1a5102d7bc66aefd0.js.gz
218
219
  - ui/dist/manifest.json
219
220
  homepage:
220
221
  licenses: