motor-admin 0.1.87 → 0.1.88

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4920715130ea25a1b4c1d8a0431d8a8753c62461b917f62ea71a7975d4c83ef9
4
- data.tar.gz: 8bba1a8fad90615e4710977ccd0990ee3eeb77c8da3cdc37e09d7e88a57b97d9
3
+ metadata.gz: 415aa5a525bed1a18e83449929aa31d2ff26efbeb74e5fb2fd34aabe647917ff
4
+ data.tar.gz: a3374ce520a91cf756d1062db6fdde8ac157c36b0d5bdcd6b2a35549a4af3a49
5
5
  SHA512:
6
- metadata.gz: f31c100b9fa308d083e41a9c6ee7b36dd8276e3c9184194507028e5f5966bdbaa07935942350f208e80a61c2fa8074b94e68c6708c81be6e2b5807f55e279216
7
- data.tar.gz: 9d641fcc39f3a76a45da2b57c70e45a95816f03d8ae6c279636769139f47063a2d2441d384618b6337220bf216f942e222d21fb17ba26d6dea944187cd027f58
6
+ metadata.gz: 2c49ec2643ab788bd85cf9d3d405e2c57a0a79c5bd87cbe484546d80b20373ba3699810e34d980a0e577d897f2dc78c9d925c1310b1048d18a00fc837fa9e177
7
+ data.tar.gz: 5f15967763bc7b572e0660e607db814f9939757c0c2d544a37b4e5090177969b255f2bc0d490b560f5ff42c24008a4b98f188768edfd5f19b93a322a05bbe6af
@@ -13,7 +13,10 @@ module Motor
13
13
 
14
14
  def resource_class
15
15
  @resource_class ||=
16
- Motor::BuildSchema::Utils.classify_slug(resource_name_prefix + params[:resource])
16
+ Motor::Resources::FetchConfiguredModel.call(
17
+ Motor::BuildSchema::Utils.classify_slug(resource_name_prefix + params[:resource]),
18
+ cache_key: Motor::Resource.maximum(:updated_at)
19
+ )
17
20
  end
18
21
 
19
22
  def resource_name_prefix
@@ -40,8 +43,6 @@ module Motor
40
43
  ).load_and_authorize_resource
41
44
  rescue ActiveRecord::RecordNotFound
42
45
  head :not_found
43
- rescue StandardError => e
44
- render json: { errors: [e.message] }, status: :unprocessable_entity
45
46
  end
46
47
 
47
48
  def load_and_authorize_association
@@ -63,8 +64,6 @@ module Motor
63
64
  end
64
65
  rescue ActiveRecord::RecordNotFound
65
66
  head :not_found
66
- rescue StandardError => e
67
- render json: { errors: [e.message] }, status: :unprocessable_entity
68
67
  end
69
68
  end
70
69
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ResourceDefaultQueriesController < ApiBaseController
5
+ skip_authorization_check
6
+
7
+ before_action :authorize_resource
8
+
9
+ def show
10
+ render json: { data: { sql: resource_class.all.to_sql } }
11
+ end
12
+
13
+ private
14
+
15
+ def resource_class
16
+ @resource_class ||= Motor::BuildSchema::Utils.classify_slug(params[:resource])
17
+ end
18
+
19
+ def authorize_resource
20
+ authorize!(resource_class, :manage)
21
+ end
22
+ end
23
+ end
@@ -11,7 +11,7 @@ module Motor
11
11
  end
12
12
 
13
13
  def create
14
- Motor::BuildSchema::PersistResourceConfigs.call(@resource)
14
+ Motor::Resources::PersistConfigs.call(@resource)
15
15
  Motor::Configs::WriteToFile.call
16
16
 
17
17
  render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params, current_ability) }
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class SchemaController < ApiBaseController
5
+ skip_authorization_check
6
+
7
+ before_action :authorize_resource, only: :show
8
+
9
+ def index
10
+ render json: { data: schema }
11
+ end
12
+
13
+ def show
14
+ render json: { data: schema.find { |model| model[:name] == resource_class.name.underscore } }
15
+ end
16
+
17
+ private
18
+
19
+ def schema
20
+ @schema ||= Motor::BuildSchema.call(Configs::LoadFromCache.load_cache_keys, current_ability)
21
+ end
22
+
23
+ def resource_class
24
+ @resource_class ||= Motor::BuildSchema::Utils.classify_slug(params[:resource])
25
+ end
26
+
27
+ def authorize_resource
28
+ authorize!(resource_class, :read)
29
+ end
30
+ end
31
+ end
@@ -238,3 +238,14 @@ en:
238
238
  field: Field
239
239
  empty: Empty
240
240
  not_empty: Not empty
241
+ general: General
242
+ run: Run
243
+ use_default: Use Default
244
+ query_editor: Query Editor
245
+ polymorphic: Polymorphic
246
+ foreign_key: Foreign key
247
+ primary_key: Primary key
248
+ through: Through
249
+ source: Source
250
+ resource: Resource
251
+ add_association: Add Association
@@ -238,6 +238,17 @@ es:
238
238
  field: Campo
239
239
  empty: Vacío
240
240
  not_empty: No vacío
241
+ general: General
242
+ run: Ejecutar
243
+ use_default: Uso por defecto
244
+ query_editor: Editor de consultas
245
+ polymorphic: Polimórfico
246
+ foreign_key: Clave externa
247
+ primary_key: Clave primaria
248
+ through: Mediante
249
+ source: Fuente
250
+ resource: Recurso
251
+ add_association: Añadir Asociación
241
252
  i:
242
253
  locale: es
243
254
  select:
data/config/routes.rb CHANGED
@@ -11,12 +11,13 @@ Motor::Admin.routes.draw do
11
11
  resources :configs, only: %i[index create]
12
12
  resources :resources, only: %i[index create]
13
13
  resources :resource_methods, only: %i[show], param: 'resource'
14
+ resources :resource_default_queries, only: %i[show], param: 'resource'
15
+ resources :schema, only: %i[index show], param: 'resource'
14
16
  resources :dashboards, only: %i[index show create update destroy]
15
17
  resources :forms, only: %i[index show create update destroy]
16
18
  resources :alerts, only: %i[index show create update destroy]
17
19
  resources :icons, only: %i[index]
18
20
  resources :active_storage_attachments, only: %i[create], path: 'data/active_storage__attachments'
19
- resource :schema, only: %i[show update]
20
21
  resources :audits, only: %i[index]
21
22
  resources :resources, path: '/data/:resource',
22
23
  only: %i[index show update create destroy],
data/lib/motor.rb CHANGED
@@ -25,6 +25,7 @@ module Motor
25
25
  next if f.ends_with?('alerts/scheduled_alerts_cache.rb')
26
26
  next if f.ends_with?('configs/load_from_cache.rb')
27
27
  next if f.ends_with?('configs/sync_from_file.rb')
28
+ next if f.ends_with?('resources/custom_sql_columns_cache.rb')
28
29
 
29
30
  load f
30
31
  end
@@ -62,6 +63,7 @@ require 'motor/queries'
62
63
  require 'motor/dashboards'
63
64
  require 'motor/forms'
64
65
  require 'motor/alerts'
66
+ require 'motor/resources'
65
67
  require 'motor/hash_serializer'
66
68
  require 'motor/net_http_utils'
67
69
  require 'motor/railtie'
@@ -3,6 +3,12 @@
3
3
  module Motor
4
4
  module ActiveRecordUtils
5
5
  module DefinedScopesExtension
6
+ def inherited(subclass)
7
+ super
8
+
9
+ subclass.instance_variable_set(:@__scopes__, subclass.superclass.instance_variable_get(:@__scopes__).dup)
10
+ end
11
+
6
12
  def scope(name, _body)
7
13
  (@__scopes__ ||= []) << name.to_sym
8
14
 
@@ -36,8 +36,9 @@ module Motor
36
36
  all.invert[name.to_s]
37
37
  end
38
38
 
39
- def find_name_for_class(klass)
40
- name = all[klass.to_s]
39
+ def find_name_for_type(type)
40
+ name = all[type.subtype.class.to_s] if type.respond_to?(:subtype)
41
+ name ||= all[type.class.to_s]
41
42
 
42
43
  return UNIFIED_TYPES.fetch(name, name) if name
43
44
 
@@ -78,7 +78,7 @@ module Motor
78
78
 
79
79
  next if !is_permitted_all && permitted_attributes.exclude?(field_symbol)
80
80
 
81
- if column_names.include?(field_symbol)
81
+ if model.columns_hash[field]
82
82
  acc['only'] << field
83
83
  elsif instance_methods.include?(field_symbol)
84
84
  acc['methods'] << field
@@ -15,7 +15,8 @@ module Motor
15
15
  ALL = [
16
16
  STRING = 'string',
17
17
  INTEGER = 'integer',
18
- DECIMAL = 'float',
18
+ FLOAT = 'float',
19
+ REFERENCE = 'reference',
19
20
  DATETIME = 'datetime',
20
21
  DATE = 'date',
21
22
  BOOLEAN = 'boolean',
@@ -34,8 +35,19 @@ module Motor
34
35
  ].freeze
35
36
  end
36
37
 
38
+ module ColumnSources
39
+ ALL = [
40
+ TABLE = 'table',
41
+ QUERY = 'query',
42
+ REFLECTION = 'reflection'
43
+ ].freeze
44
+ end
45
+
37
46
  SEARCHABLE_COLUMN_TYPES = %i[citext text string bitstring].freeze
38
47
 
48
+ DEFAULT_TYPE = 'default'
49
+ DEFAULT_ICON = 'database'
50
+
39
51
  COLUMN_NAME_ACCESS_TYPES = {
40
52
  id: ColumnAccessTypes::READ_ONLY,
41
53
  created_at: ColumnAccessTypes::READ_ONLY,
@@ -43,7 +55,37 @@ module Motor
43
55
  deleted_at: ColumnAccessTypes::READ_ONLY
44
56
  }.with_indifferent_access.freeze
45
57
 
46
- DEFAULT_SCOPE_TYPE = 'default'
58
+ COLUMN_DEFAULTS = {
59
+ access_type: ColumnAccessTypes::READ_WRITE,
60
+ column_source: ColumnSources::TABLE,
61
+ default_value: nil,
62
+ reference: nil,
63
+ format: {},
64
+ validators: []
65
+ }.with_indifferent_access
66
+
67
+ ACTION_DEFAULTS = {
68
+ visible: true,
69
+ preferences: {}
70
+ }.with_indifferent_access
71
+
72
+ TAB_DEFAULTS = {
73
+ visible: true,
74
+ tab_type: DEFAULT_TYPE,
75
+ preferences: {}
76
+ }.with_indifferent_access
77
+
78
+ SCOPE_DEFAULTS = {
79
+ visible: true,
80
+ scope_type: DEFAULT_TYPE,
81
+ preferences: {}
82
+ }.with_indifferent_access
83
+
84
+ ASSOCIATION_DEFAULTS = {
85
+ visible: true,
86
+ icon: DEFAULT_ICON,
87
+ options: {}
88
+ }.with_indifferent_access
47
89
 
48
90
  module_function
49
91
 
@@ -62,7 +104,6 @@ require_relative './build_schema/adjust_devise_model_schema'
62
104
  require_relative './build_schema/load_from_rails'
63
105
  require_relative './build_schema/find_display_column'
64
106
  require_relative './build_schema/find_icon'
65
- require_relative './build_schema/persist_resource_configs'
66
107
  require_relative './build_schema/reorder_schema'
67
108
  require_relative './build_schema/merge_schema_configs'
68
109
  require_relative './build_schema/apply_permissions'
@@ -11,21 +11,21 @@ module Motor
11
11
  {
12
12
  name: 'create',
13
13
  display_name: I18n.t('motor.create'),
14
- action_type: 'default',
14
+ action_type: BuildSchema::DEFAULT_TYPE,
15
15
  preferences: {},
16
16
  visible: true
17
17
  },
18
18
  {
19
19
  name: 'edit',
20
20
  display_name: I18n.t('motor.edit'),
21
- action_type: 'default',
21
+ action_type: BuildSchema::DEFAULT_TYPE,
22
22
  preferences: {},
23
23
  visible: true
24
24
  },
25
25
  {
26
26
  name: 'remove',
27
27
  display_name: I18n.t('motor.remove'),
28
- action_type: 'default',
28
+ action_type: BuildSchema::DEFAULT_TYPE,
29
29
  preferences: {},
30
30
  visible: true
31
31
  }
@@ -38,7 +38,7 @@ module Motor
38
38
  {
39
39
  name: 'details',
40
40
  display_name: I18n.t('motor.details'),
41
- tab_type: 'default',
41
+ tab_type: BuildSchema::DEFAULT_TYPE,
42
42
  preferences: {},
43
43
  visible: true
44
44
  }
@@ -114,12 +114,12 @@ module Motor
114
114
  'status' => 'hash'
115
115
  }.freeze
116
116
 
117
- DEFAULT_ICON = 'database'
117
+ DEFAULT_ICON = BuildSchema::DEFAULT_ICON
118
118
 
119
119
  module_function
120
120
 
121
121
  def call(text)
122
- text = text.downcase
122
+ text = text.underscore
123
123
  icon = ICONS_MAP[text] || ICONS_MAP[text.singularize]
124
124
 
125
125
  icon ||=
@@ -43,7 +43,7 @@ module Motor
43
43
  def models
44
44
  eager_load_models!
45
45
 
46
- models = ActiveRecord::Base.descendants.reject(&:abstract_class)
46
+ models = ActiveRecord::Base.descendants.reject { |k| k.abstract_class || k.anonymous? }
47
47
 
48
48
  models -= Motor::ApplicationRecord.descendants
49
49
  models -= [Motor::Audit]
@@ -75,6 +75,7 @@ module Motor
75
75
  scopes: fetch_scopes(model),
76
76
  actions: BuildSchema::Defaults.actions,
77
77
  tabs: BuildSchema::Defaults.tabs,
78
+ custom_sql: nil,
78
79
  visible: true
79
80
  }.with_indifferent_access
80
81
  end
@@ -91,7 +92,7 @@ module Motor
91
92
  display_name: I18n.t(scope_name,
92
93
  scope: [I18N_SCOPES_KEY, model.name.underscore].join('.'),
93
94
  default: scope_name.humanize),
94
- scope_type: DEFAULT_SCOPE_TYPE,
95
+ scope_type: DEFAULT_TYPE,
95
96
  visible: true,
96
97
  preferences: {}
97
98
  }
@@ -118,6 +119,7 @@ module Motor
118
119
  name: column.name,
119
120
  display_name: Utils.humanize_column_name(model.human_attribute_name(column.name)),
120
121
  column_type: fetch_column_type(column, model),
122
+ column_source: ColumnSources::TABLE,
121
123
  is_array: column.array?,
122
124
  access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
123
125
  default_value: default_attrs[column.name],
@@ -173,6 +175,7 @@ module Motor
173
175
  end.compact
174
176
  end
175
177
 
178
+ # rubocop:disable Metrics/AbcSize
176
179
  def build_reflection_column(name, model, ref, default_attrs)
177
180
  if !ref.polymorphic? && ref.klass.name == 'ActionText::RichText'
178
181
  return build_action_text_column(name, model, ref)
@@ -181,11 +184,14 @@ module Motor
181
184
  column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
182
185
  is_attachment = !ref.polymorphic? && ref.klass.name == 'ActiveStorage::Attachment'
183
186
  access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
187
+ column_type = is_attachment ? ColumnTypes::FILE : ColumnTypes::REFERENCE
188
+ column_source = model.columns_hash[column_name] ? ColumnSources::TABLE : ColumnSources::REFLECTION
184
189
 
185
190
  {
186
191
  name: column_name,
187
192
  display_name: model.human_attribute_name(name),
188
- column_type: is_attachment ? ColumnTypes::FILE : ColumnTypes::INTEGER,
193
+ column_type: column_type,
194
+ column_source: column_source,
189
195
  access_type: access_type,
190
196
  default_value: default_attrs[column_name],
191
197
  validators: fetch_validators(model, column_name, ref),
@@ -194,6 +200,7 @@ module Motor
194
200
  virtual: false
195
201
  }
196
202
  end
203
+ # rubocop:enable Metrics/AbcSize
197
204
 
198
205
  def build_action_text_column(name, model, ref)
199
206
  name = name.delete_prefix(ACTION_TEXT_REFLECTION_PREFIX)
@@ -202,6 +209,7 @@ module Motor
202
209
  name: name + ACTION_TEXT_COLUMN_SUFFIX,
203
210
  display_name: model.human_attribute_name(name),
204
211
  column_type: ColumnTypes::RICHTEXT,
212
+ column_source: ColumnSources::REFLECTION,
205
213
  access_type: ColumnAccessTypes::READ_WRITE,
206
214
  default_value: '',
207
215
  validators: fetch_validators(model, name, ref),
@@ -218,11 +226,14 @@ module Motor
218
226
  model_name: reflection.polymorphic? ? nil : reflection.klass.name.underscore,
219
227
  reference_type: reflection.belongs_to? ? 'belongs_to' : 'has_one',
220
228
  foreign_key: reflection.foreign_key,
221
- association_primary_key: reflection.polymorphic? ? 'id' : reflection.association_primary_key,
222
- polymorphic: reflection.polymorphic?
229
+ primary_key: reflection.polymorphic? ? 'id' : reflection.active_record_primary_key,
230
+ options: reflection.options.slice(:through, :source),
231
+ polymorphic: reflection.polymorphic?,
232
+ virtual: false
223
233
  }
224
234
  end
225
235
 
236
+ # rubocop:disable Metrics/AbcSize
226
237
  def fetch_associations(model)
227
238
  model.reflections.map do |name, ref|
228
239
  next if ref.has_one? || ref.belongs_to?
@@ -238,12 +249,16 @@ module Motor
238
249
  slug: name.underscore,
239
250
  model_name: model_class.name.underscore,
240
251
  foreign_key: ref.foreign_key,
252
+ primary_key: ref.active_record_primary_key,
241
253
  polymorphic: ref.options[:as].present?,
242
254
  icon: Motor::FindIcon.call(name),
255
+ options: ref.options.slice(:through, :source),
256
+ virtual: false,
243
257
  visible: true
244
258
  }
245
259
  end.compact
246
260
  end
261
+ # rubocop:enable Metrics/AbcSize
247
262
 
248
263
  def fetch_validators(model, column_name, reflection = nil)
249
264
  validators =
@@ -3,12 +3,6 @@
3
3
  module Motor
4
4
  module BuildSchema
5
5
  module MergeSchemaConfigs
6
- RESOURCE_ATTRS = PersistResourceConfigs::RESOURCE_ATTRS
7
- COLUMN_DEFAULTS = PersistResourceConfigs::COLUMN_DEFAULTS
8
- ACTION_DEFAULTS = PersistResourceConfigs::ACTION_DEFAULTS
9
- TAB_DEFAULTS = PersistResourceConfigs::TAB_DEFAULTS
10
- SCOPE_DEFAULTS = PersistResourceConfigs::SCOPE_DEFAULTS
11
-
12
6
  module_function
13
7
 
14
8
  # @param schema [Array<HashWithIndifferentAccess>]
@@ -26,7 +20,7 @@ module Motor
26
20
  # @param configs [HashWithIndifferentAccess]
27
21
  # @return [HashWithIndifferentAccess]
28
22
  def merge_model(model, configs)
29
- updated_model = model.merge(configs.slice(*RESOURCE_ATTRS))
23
+ updated_model = model.merge(configs.slice(*Resources::RESOURCE_ATTRS))
30
24
 
31
25
  merge_actions!(updated_model, configs)
32
26
  merge_associations!(updated_model, configs)
@@ -44,8 +38,8 @@ module Motor
44
38
  model[:associations] = merge_by_name(
45
39
  model[:associations],
46
40
  configs[:associations],
47
- {},
48
- ->(_) { true }
41
+ ASSOCIATION_DEFAULTS,
42
+ ->(scope) { !scope[:virtual] }
49
43
  )
50
44
 
51
45
  model
@@ -56,7 +50,7 @@ module Motor
56
50
  # @return [HashWithIndifferentAccess]
57
51
  def merge_columns!(model, configs)
58
52
  model[:columns] = merge_by_name(
59
- model[:columns],
53
+ merge_custom_sql_columns(model[:columns], configs[:custom_sql]),
60
54
  configs[:columns],
61
55
  COLUMN_DEFAULTS,
62
56
  ->(scope) { !scope[:virtual] }
@@ -121,6 +115,34 @@ module Motor
121
115
  end.compact
122
116
  end
123
117
 
118
+ # @param columns [Array<HashWithIndifferentAccess>]
119
+ # @param sql [String]
120
+ # @return [Array<HashWithIndifferentAccess>]
121
+ def merge_custom_sql_columns(columns, sql)
122
+ return columns if sql.blank?
123
+
124
+ query_columns = Resources::CustomSqlColumnsCache.call(sql)
125
+
126
+ columns_index = columns.index_by { |e| e[:name] }
127
+
128
+ merged_columns =
129
+ query_columns.map do |column|
130
+ existing_column = columns_index.delete(column[:name])
131
+
132
+ next existing_column if existing_column
133
+
134
+ column.merge(COLUMN_DEFAULTS).merge(
135
+ access_type: ColumnAccessTypes::READ_ONLY,
136
+ column_source: ColumnSources::QUERY,
137
+ virtual: false
138
+ )
139
+ end
140
+
141
+ reflection_columns = columns_index.values.select { |c| c[:column_source] == ColumnSources::REFLECTION }
142
+
143
+ reflection_columns + merged_columns
144
+ end
145
+
124
146
  # @param cache_keys [Hash]
125
147
  # @return [HashWithIndifferentAccess<String, HashWithIndifferentAccess>]
126
148
  def load_configs(cache_keys)
@@ -84,12 +84,13 @@ module Motor
84
84
  # @return [Hash]
85
85
  def build_columns_hash(result)
86
86
  result.columns.map do |column_name|
87
- column_type_class = result.column_types[column_name].class
87
+ column_type = result.column_types[column_name]
88
88
 
89
89
  {
90
90
  name: column_name,
91
91
  display_name: column_name.humanize,
92
- column_type: ActiveRecordUtils::Types.find_name_for_class(column_type_class)
92
+ column_type: ActiveRecordUtils::Types.find_name_for_type(column_type),
93
+ is_array: column_type.class.to_s == 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array'
93
94
  }
94
95
  end
95
96
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Resources
5
+ RESOURCE_ATTRS = %w[display_name icon custom_sql visible].freeze
6
+ COLUMN_ATTRS = %w[name display_name column_type access_type default_value reference virtual format].freeze
7
+ ASSOCIATION_ATTRS = %w[name display_name model_name icon visible foreign_key primary_key options virtual
8
+ polymorphic slug].freeze
9
+ SCOPE_ATTRS = %w[name display_name scope_type preferences visible].freeze
10
+ ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
11
+ TAB_ATTRS = %w[name display_name tab_type preferences visible].freeze
12
+ end
13
+ end
14
+
15
+ require_relative './resources/fetch_configured_model'
16
+ require_relative './resources/persist_configs'
17
+ require_relative './resources/custom_sql_columns_cache'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Resources
5
+ module CustomSqlColumnsCache
6
+ CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
7
+
8
+ module_function
9
+
10
+ def call(sql)
11
+ CACHE_STORE.fetch(sql.hash) do
12
+ Queries::RunQuery.call(Query.new(sql_body: sql), limit: 0).columns
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Resources
5
+ module FetchConfiguredModel
6
+ CACHE_HASH = HashWithIndifferentAccess.new
7
+
8
+ module_function
9
+
10
+ def call(model, cache_key:)
11
+ configs = Motor::Configs::LoadFromCache.load_resources(cache_key: cache_key)
12
+
13
+ maybe_fetch_from_cache(
14
+ model,
15
+ cache_key,
16
+ lambda {
17
+ resource_config = configs.find { |r| r.name == model.name.underscore }
18
+
19
+ if resource_config
20
+ build_configured_model(model, resource_config.preferences)
21
+ else
22
+ define_class_name_method(Class.new(model), model)
23
+ end
24
+ },
25
+ ->(klass) { configure_reflection_classes(klass, cache_key) }
26
+ )
27
+ end
28
+
29
+ def build_configured_model(model, config)
30
+ klass = Class.new(model)
31
+
32
+ define_class_name_method(klass, model)
33
+
34
+ define_columns_hash(klass, config)
35
+ define_default_scope(klass, config)
36
+ define_column_reflections(klass, config)
37
+ define_associations(klass, config)
38
+
39
+ klass
40
+ end
41
+
42
+ def define_default_scope(klass, config)
43
+ return klass if config[:custom_sql].blank?
44
+
45
+ klass.instance_variable_set(:@__motor_custom_sql, config[:custom_sql].squish)
46
+
47
+ klass.instance_eval do
48
+ default_scope do
49
+ from(Arel.sql("(#{self.klass.instance_variable_get(:@__motor_custom_sql)})").as(table_name))
50
+ end
51
+ end
52
+
53
+ klass
54
+ end
55
+
56
+ def define_class_name_method(klass, model)
57
+ klass.instance_variable_set(:@__motor_model_name, model.name)
58
+
59
+ klass.instance_eval do
60
+ def name
61
+ @__motor_model_name
62
+ end
63
+
64
+ def inspect
65
+ super.gsub(/\#<Class:0x\w+>/, name)
66
+ end
67
+
68
+ def to_s
69
+ super.gsub(/\#<Class:0x\w+>/, name)
70
+ end
71
+
72
+ def anonymous?
73
+ true
74
+ end
75
+ end
76
+
77
+ klass
78
+ end
79
+
80
+ def define_columns_hash(klass, config)
81
+ return klass if config[:custom_sql].blank?
82
+
83
+ columns = Resources::CustomSqlColumnsCache.call(config[:custom_sql])
84
+
85
+ columns_hash =
86
+ columns.each_with_object({}) do |column, acc|
87
+ acc[column[:name]] = ActiveRecord::ConnectionAdapters::Column.new(column[:name], nil)
88
+ end
89
+
90
+ klass.instance_variable_set(:@__motor_custom_sql_columns_hash, columns_hash)
91
+
92
+ # rubocop:disable Naming/MemoizedInstanceVariableName
93
+ klass.instance_eval do
94
+ def columns_hash
95
+ @__motor__columns_hash ||= @__motor_custom_sql_columns_hash.merge(super)
96
+ end
97
+ end
98
+ # rubocop:enable Naming/MemoizedInstanceVariableName
99
+ end
100
+
101
+ def define_column_reflections(klass, config)
102
+ config.fetch(:columns, []).each do |column|
103
+ reference = column[:reference]
104
+
105
+ next if reference.blank?
106
+ next unless reference[:virtual]
107
+
108
+ if reference[:reference_type] == 'belongs_to'
109
+ define_belongs_to_reflection(klass, reference)
110
+ else
111
+ define_has_one_reflection(klass, reference)
112
+ end
113
+ end
114
+ end
115
+
116
+ def define_belongs_to_reflection(klass, config)
117
+ klass.belongs_to(config[:name].to_sym,
118
+ class_name: config[:model_name].classify,
119
+ foreign_key: config[:foreign_key],
120
+ polymorphic: config[:polymorphic],
121
+ primary_key: config[:primary_key],
122
+ optional: true)
123
+ end
124
+
125
+ def define_has_one_reflection(klass, config)
126
+ options = {
127
+ class_name: config[:model_name].classify,
128
+ foreign_key: config[:foreign_key],
129
+ primary_key: config[:primary_key]
130
+ }
131
+
132
+ options = options.merge(config[:options] || {})
133
+
134
+ klass.has_one(config[:name].to_sym, **options.symbolize_keys)
135
+ end
136
+
137
+ def configure_reflection_classes(klass, cache_key)
138
+ klass.reflections.each do |key, ref|
139
+ begin
140
+ next unless ref.klass
141
+ next if ref.klass.anonymous?
142
+ rescue StandardError
143
+ next
144
+ end
145
+
146
+ ref_dup = ref.dup
147
+
148
+ if ref.klass.name == klass.name
149
+ ref_dup.instance_variable_set(:@klass, klass)
150
+ else
151
+ ref_dup.instance_variable_set(:@klass, call(ref.klass, cache_key: cache_key))
152
+ end
153
+
154
+ klass.reflections[key] = ref_dup
155
+ end
156
+
157
+ klass._reflections = klass.reflections
158
+
159
+ klass
160
+ end
161
+
162
+ def define_associations(klass, config)
163
+ config.fetch(:associations, []).each do |association|
164
+ is_virtual, is_polymorphic = association.values_at(:virtual, :polymorphic)
165
+
166
+ next unless is_virtual
167
+
168
+ options = association.slice(:foreign_key, :primary_key)
169
+ options[:class_name] = association[:model_name].classify
170
+ options[:as] = association[:foreign_key].delete_suffix('_id') if is_polymorphic
171
+
172
+ options = options.merge(association[:options] || {})
173
+
174
+ klass.has_many(association[:name].to_sym, **options.symbolize_keys)
175
+ end
176
+ end
177
+
178
+ def maybe_fetch_from_cache(model, cache_key, miss_cache_block, postprocess_block)
179
+ return yield unless cache_key
180
+
181
+ if CACHE_HASH[model.name] && CACHE_HASH[model.name][:key] == cache_key
182
+ CACHE_HASH[model.name][:value]
183
+ else
184
+ result = miss_cache_block.call
185
+
186
+ CACHE_HASH[model.name] = { key: cache_key, value: result }
187
+
188
+ postprocess_block.call(result)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -1,39 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- module BuildSchema
5
- module PersistResourceConfigs
6
- RESOURCE_ATTRS = %w[display_name icon visible].freeze
7
- COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual format].freeze
8
- ASSOCIATION_ATTRS = %w[name display_name icon visible].freeze
9
- SCOPE_ATTRS = %w[name display_name scope_type preferences visible].freeze
10
- ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
11
- TAB_ATTRS = %w[name display_name tab_type preferences visible].freeze
12
-
13
- COLUMN_DEFAULTS = {
14
- access_type: 'read_write',
15
- default_value: nil,
16
- reference: nil,
17
- format: {},
18
- validators: []
19
- }.with_indifferent_access
20
-
21
- ACTION_DEFAULTS = {
22
- visible: true,
23
- preferences: {}
24
- }.with_indifferent_access
25
-
26
- TAB_DEFAULTS = {
27
- visible: true,
28
- tab_type: 'default',
29
- preferences: {}
30
- }.with_indifferent_access
31
-
32
- SCOPE_DEFAULTS = {
33
- visible: true,
34
- scope_type: 'default',
35
- preferences: {}
36
- }.with_indifferent_access
4
+ module Resources
5
+ module PersistConfigs
6
+ COLUMN_DEFAULTS = BuildSchema::COLUMN_DEFAULTS
7
+ ACTION_DEFAULTS = BuildSchema::ACTION_DEFAULTS
8
+ TAB_DEFAULTS = BuildSchema::TAB_DEFAULTS
9
+ SCOPE_DEFAULTS = BuildSchema::SCOPE_DEFAULTS
10
+ ASSOCIATION_DEFAULTS = BuildSchema::ASSOCIATION_DEFAULTS
37
11
 
38
12
  module_function
39
13
 
@@ -206,12 +180,15 @@ module Motor
206
180
  def normalize_associations(default_assocs, existing_assocs, new_assocs)
207
181
  (existing_assocs.pluck(:name) + new_assocs.pluck(:name)).uniq.map do |name|
208
182
  new_assoc = safe_fetch_by_name(new_assocs, name)
183
+
184
+ next if new_assoc[:_remove]
185
+
209
186
  existing_assoc = safe_fetch_by_name(existing_assocs, name)
210
187
  default_assoc = safe_fetch_by_name(default_assocs, name)
211
188
  assoc_attrs = new_assoc.slice(*ASSOCIATION_ATTRS)
212
189
 
213
190
  normalized_assoc = existing_assoc.merge(assoc_attrs)
214
- normalized_assoc = reject_default(default_assoc, normalized_assoc)
191
+ normalized_assoc = reject_default(default_assoc.presence || ASSOCIATION_DEFAULTS, normalized_assoc)
215
192
 
216
193
  normalized_assoc.merge(name: name) if normalized_assoc.present?
217
194
  end.compact.presence
@@ -230,7 +207,9 @@ module Motor
230
207
  # @param resource_name [String]
231
208
  # @return [HashWithIndifferentAccess]
232
209
  def fetch_default_schema(resource_name)
233
- LoadFromRails.build_model_schema(resource_name.classify.constantize)
210
+ model = resource_name.classify.constantize
211
+
212
+ BuildSchema::LoadFromRails.build_model_schema(model).merge(custom_sql: model.all.to_sql)
234
213
  end
235
214
 
236
215
  # @param default [HashWithIndifferentAccess]
@@ -240,7 +219,12 @@ module Motor
240
219
  return new unless default
241
220
 
242
221
  new.reject do |key, value|
243
- default[key].to_json == value.to_json
222
+ default[key].to_json ==
223
+ if value.is_a?(Hash) || value.is_a?(ActiveSupport::HashWithIndifferentAccess)
224
+ value.select { |_, v| v.present? }.to_json
225
+ else
226
+ value.to_json
227
+ end
244
228
  end
245
229
  end
246
230
  end
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.87'
4
+ VERSION = '0.1.88'
5
5
  end
@@ -2068,11 +2068,11 @@
2068
2068
  "mail-opened.svg": "icons/mail-opened.svg",
2069
2069
  "mail.svg": "icons/mail.svg",
2070
2070
  "mailbox.svg": "icons/mailbox.svg",
2071
- "main-8ddee666c462389f6118.css.gz": "main-8ddee666c462389f6118.css.gz",
2072
- "main-8ddee666c462389f6118.js.LICENSE.txt": "main-8ddee666c462389f6118.js.LICENSE.txt",
2073
- "main-8ddee666c462389f6118.js.gz": "main-8ddee666c462389f6118.js.gz",
2074
- "main.css": "main-8ddee666c462389f6118.css",
2075
- "main.js": "main-8ddee666c462389f6118.js",
2071
+ "main-fbe1b5253d0be9bdb20b.css.gz": "main-fbe1b5253d0be9bdb20b.css.gz",
2072
+ "main-fbe1b5253d0be9bdb20b.js.LICENSE.txt": "main-fbe1b5253d0be9bdb20b.js.LICENSE.txt",
2073
+ "main-fbe1b5253d0be9bdb20b.js.gz": "main-fbe1b5253d0be9bdb20b.js.gz",
2074
+ "main.css": "main-fbe1b5253d0be9bdb20b.css",
2075
+ "main.js": "main-fbe1b5253d0be9bdb20b.js",
2076
2076
  "man.svg": "icons/man.svg",
2077
2077
  "manual-gearbox.svg": "icons/manual-gearbox.svg",
2078
2078
  "map-2.svg": "icons/map-2.svg",
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.87
4
+ version: 0.1.88
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-07-14 00:00:00.000000000 Z
11
+ date: 2021-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -138,9 +138,11 @@ files:
138
138
  - app/controllers/motor/forms_controller.rb
139
139
  - app/controllers/motor/icons_controller.rb
140
140
  - app/controllers/motor/queries_controller.rb
141
+ - app/controllers/motor/resource_default_queries_controller.rb
141
142
  - app/controllers/motor/resource_methods_controller.rb
142
143
  - app/controllers/motor/resources_controller.rb
143
144
  - app/controllers/motor/run_queries_controller.rb
145
+ - app/controllers/motor/schema_controller.rb
144
146
  - app/controllers/motor/send_alerts_controller.rb
145
147
  - app/controllers/motor/tags_controller.rb
146
148
  - app/controllers/motor/ui_controller.rb
@@ -202,7 +204,6 @@ files:
202
204
  - lib/motor/build_schema/find_icon.rb
203
205
  - lib/motor/build_schema/load_from_rails.rb
204
206
  - lib/motor/build_schema/merge_schema_configs.rb
205
- - lib/motor/build_schema/persist_resource_configs.rb
206
207
  - lib/motor/build_schema/reorder_schema.rb
207
208
  - lib/motor/build_schema/utils.rb
208
209
  - lib/motor/cancan_utils.rb
@@ -229,6 +230,10 @@ files:
229
230
  - lib/motor/queries/render_sql_template.rb
230
231
  - lib/motor/queries/run_query.rb
231
232
  - lib/motor/railtie.rb
233
+ - lib/motor/resources.rb
234
+ - lib/motor/resources/custom_sql_columns_cache.rb
235
+ - lib/motor/resources/fetch_configured_model.rb
236
+ - lib/motor/resources/persist_configs.rb
232
237
  - lib/motor/tags.rb
233
238
  - lib/motor/tasks/motor.rake
234
239
  - lib/motor/version.rb
@@ -1495,8 +1500,8 @@ files:
1495
1500
  - ui/dist/icons/zoom-money.svg.gz
1496
1501
  - ui/dist/icons/zoom-out.svg.gz
1497
1502
  - ui/dist/icons/zoom-question.svg.gz
1498
- - ui/dist/main-8ddee666c462389f6118.css.gz
1499
- - ui/dist/main-8ddee666c462389f6118.js.gz
1503
+ - ui/dist/main-fbe1b5253d0be9bdb20b.css.gz
1504
+ - ui/dist/main-fbe1b5253d0be9bdb20b.js.gz
1500
1505
  - ui/dist/manifest.json
1501
1506
  homepage:
1502
1507
  licenses: