motor-admin 0.1.87 → 0.1.88

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: 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: