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 +4 -4
- data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +4 -5
- data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
- data/app/controllers/motor/resources_controller.rb +1 -1
- data/app/controllers/motor/schema_controller.rb +31 -0
- data/config/locales/en.yml +11 -0
- data/config/locales/es.yml +11 -0
- data/config/routes.rb +2 -1
- data/lib/motor.rb +2 -0
- data/lib/motor/active_record_utils/defined_scopes_extension.rb +6 -0
- data/lib/motor/active_record_utils/types.rb +3 -2
- data/lib/motor/api_query/build_json.rb +1 -1
- data/lib/motor/build_schema.rb +44 -3
- data/lib/motor/build_schema/defaults.rb +4 -4
- data/lib/motor/build_schema/find_icon.rb +2 -2
- data/lib/motor/build_schema/load_from_rails.rb +20 -5
- data/lib/motor/build_schema/merge_schema_configs.rb +32 -10
- data/lib/motor/queries/run_query.rb +3 -2
- data/lib/motor/resources.rb +17 -0
- data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
- data/lib/motor/resources/fetch_configured_model.rb +193 -0
- data/lib/motor/{build_schema/persist_resource_configs.rb → resources/persist_configs.rb} +20 -36
- data/lib/motor/version.rb +1 -1
- data/ui/dist/{main-8ddee666c462389f6118.css.gz → main-fbe1b5253d0be9bdb20b.css.gz} +0 -0
- data/ui/dist/main-fbe1b5253d0be9bdb20b.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +10 -5
- data/ui/dist/main-8ddee666c462389f6118.js.gz +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 415aa5a525bed1a18e83449929aa31d2ff26efbeb74e5fb2fd34aabe647917ff
|
4
|
+
data.tar.gz: a3374ce520a91cf756d1062db6fdde8ac157c36b0d5bdcd6b2a35549a4af3a49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
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::
|
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
|
data/config/locales/en.yml
CHANGED
@@ -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
|
data/config/locales/es.yml
CHANGED
@@ -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
|
40
|
-
name
|
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
|
81
|
+
if model.columns_hash[field]
|
82
82
|
acc['only'] << field
|
83
83
|
elsif instance_methods.include?(field_symbol)
|
84
84
|
acc['methods'] << field
|
data/lib/motor/build_schema.rb
CHANGED
@@ -15,7 +15,8 @@ module Motor
|
|
15
15
|
ALL = [
|
16
16
|
STRING = 'string',
|
17
17
|
INTEGER = 'integer',
|
18
|
-
|
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
|
-
|
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:
|
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:
|
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:
|
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:
|
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 =
|
117
|
+
DEFAULT_ICON = BuildSchema::DEFAULT_ICON
|
118
118
|
|
119
119
|
module_function
|
120
120
|
|
121
121
|
def call(text)
|
122
|
-
text = text.
|
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
|
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:
|
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:
|
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
|
-
|
222
|
-
|
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
|
-
->(
|
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
|
-
|
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.
|
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
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
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 ==
|
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
Binary file
|
Binary file
|
data/ui/dist/manifest.json
CHANGED
@@ -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-
|
2072
|
-
"main-
|
2073
|
-
"main-
|
2074
|
-
"main.css": "main-
|
2075
|
-
"main.js": "main-
|
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.
|
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-
|
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-
|
1499
|
-
- ui/dist/main-
|
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:
|
Binary file
|