motor-admin 0.1.60 → 0.1.61

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: 5d709fb5b88f56a20cb9c2f906f3540b53278e04becc96598556dba70a0dcb4f
4
- data.tar.gz: 5ffca9ae9451e2aa373957116c1b9b6607f5aba3bc1fa1cee555682c1a2987be
3
+ metadata.gz: 32c8b159e2bc232debb27c4998d0a06b78d11cb4dee7d9080bb3557f183ca788
4
+ data.tar.gz: ec29e2a202609d5690deccb9ca2bc1d5b46438c80add5e5e6553cd9fb37bd02d
5
5
  SHA512:
6
- metadata.gz: 944babec34990e1889a52e76f652ca725dda31f4bcbb75aefc47cb715beca9a1a6127d6c5aa1e70437beafdf6a0c9cfaaaf3cf29a901524980812e23134cefaa
7
- data.tar.gz: c79c99d0491e3c02c05296f0ace0ace54bbeeef1cd11f927b49f5edd814503f3e5f0f5a017c184e97bbce9a4b02b7279db36333a8b3fb796fb6125f283e8604a
6
+ metadata.gz: aeb234e7240f4ea926c9890f45d0bf24da8e7d811919d9584523fc9bd384403bac20c893316285cce76f217596131c78766a4092ff03f6bea5977334e70f30db
7
+ data.tar.gz: 8c11a65a3e2c33bf05918e2544624498ae396fadf83e4a6ffb549f2dcf293d7217e9a740723bb4fb342c62acb6a8e8a13c11cb086e404e8341a72ae2f5a30616
data/README.md CHANGED
@@ -43,9 +43,9 @@ $ rails motor:install && rake db:migrate
43
43
 
44
44
  ![Settings UI](https://user-images.githubusercontent.com/5418788/119263883-90708780-bbe9-11eb-9f9f-f76fed0b7f27.png)
45
45
 
46
- Everything in the admin panel can be configured using intuitive settings UI, which can be opened via the icon in the top right corner.
46
+ Everything in the admin panel can be configured using the intuitive settings UI, which can be opened via the icon in the top right corner.
47
47
 
48
- Data displayed on the resource page can be completely custimized via [SQL queries](#sql-queries) and [dashboards](#dashboards) attached to the resource as a tab. Usually, queries used to display resource data should contain `{{resource_name_id}}` [variable](#sql-queries).
48
+ Data displayed on the resource page can be completely customized via [SQL queries](#sql-queries) and [dashboards](#dashboards) attached to the resource as a tab. Usually, queries used to display resource data should contain `{{resource_name_id}}` [variable](#sql-queries).
49
49
 
50
50
  ### Custom Actions
51
51
 
@@ -63,7 +63,7 @@ Values from the form fields can be used in API path via `{field_name}` syntax: `
63
63
 
64
64
  ![SQL query](https://user-images.githubusercontent.com/5418788/119264127-84d19080-bbea-11eb-9903-ef465d1d2c97.png)
65
65
 
66
- Queries can include embeded variables via `{{variable}}` syntax ([mustache](https://mustache.github.io)). `{{#variable}} ... {{/variable}}` syntax allows to decide if conditions inside the scope should be included in the query.
66
+ Queries can include embedded variables via `{{variable}}` syntax ([mustache](https://mustache.github.io)). `{{#variable}} ... {{/variable}}` syntax allows to decide if conditions inside the scope should be included in the query.
67
67
 
68
68
  ### Data Visualization
69
69
 
@@ -143,12 +143,6 @@ Start example application in development mode:
143
143
  MOTOR_DEVELOPMENT=true rails s
144
144
  ```
145
145
 
146
- ## Comming Soon
147
-
148
- * Multiple databases
149
- * NoSQL data sources
150
- * Pro Bussines intelligence features
151
-
152
146
  ## License
153
147
 
154
148
  The gem is available as open source under the terms of the [MIT License](https://github.com/omohokcoj/motor-admin/blob/master/LICENSE).
@@ -8,6 +8,7 @@ module Motor
8
8
  unless Rails.env.test?
9
9
  rescue_from StandardError do |e|
10
10
  Rails.logger.error(e)
11
+ Rails.logger.error(e.backtrace.join("\n"))
11
12
 
12
13
  render json: { errors: [e.message] }, status: :internal_server_error
13
14
  end
@@ -20,8 +20,9 @@ module Motor
20
20
  private
21
21
 
22
22
  def render_result
23
- variables = params.fetch(:variables, {}).merge(current_user_variables)
24
- query_result = Queries::RunQuery.call(@query, variables_hash: variables)
23
+ query_result = Queries::RunQuery.call(@query, variables_hash: variables_params,
24
+ limit: params[:limit].presence,
25
+ filters: filter_params)
25
26
 
26
27
  if query_result.error
27
28
  render json: { errors: [{ detail: query_result.error }] }, status: :unprocessable_entity
@@ -56,5 +57,13 @@ module Motor
56
57
  def query_params
57
58
  params.require(:data).permit(:sql_body, preferences: {})
58
59
  end
60
+
61
+ def variables_params
62
+ params.fetch(:variables, {}).merge(current_user_variables)
63
+ end
64
+
65
+ def filter_params
66
+ (params[:filter] || params[:filters])&.to_unsafe_h
67
+ end
59
68
  end
60
69
  end
@@ -149,6 +149,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
149
149
  drop_table :motor_audits
150
150
  drop_table :motor_alert_locks
151
151
  drop_table :motor_alerts
152
+ drop_table :motor_forms
152
153
  drop_table :motor_taggable_tags
153
154
  drop_table :motor_tags
154
155
  drop_table :motor_resources
@@ -20,4 +20,5 @@ require_relative './active_record_utils/fetch_methods'
20
20
  require_relative './active_record_utils/defined_scopes_extension'
21
21
  require_relative './active_record_utils/active_storage_links_extension'
22
22
  require_relative './active_record_utils/active_storage_blob_patch'
23
- require_relative './active_record_utils/active_record_filter'
23
+ require_relative './active_record_utils/active_record_filter_patch'
24
+ require_relative './active_record_utils/active_record_connection_column_patch'
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'active_record/connection_adapters/deduplicable'
5
+ rescue LoadError
6
+ nil
7
+ end
8
+
9
+ ActiveRecord::ConnectionAdapters::Column.class_eval do
10
+ def array
11
+ false
12
+ end
13
+ end
data/lib/motor/admin.rb CHANGED
@@ -45,6 +45,7 @@ module Motor
45
45
  initializer 'motor.alerts.scheduler' do
46
46
  config.after_initialize do
47
47
  next unless Motor.server?
48
+ next if Motor.development?
48
49
 
49
50
  Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
50
51
  end
@@ -5,9 +5,9 @@ module Motor
5
5
  module_function
6
6
 
7
7
  def call(rel, params)
8
- rel = ApiQuery::Sort.call(rel, params[:sort])
8
+ rel = ApiQuery::Sort.call(rel, params[:sort] || params[:order])
9
9
  rel = ApiQuery::Paginate.call(rel, params[:page])
10
- rel = ApiQuery::Filter.call(rel, params[:filter])
10
+ rel = ApiQuery::Filter.call(rel, params[:filter] || params[:filters])
11
11
  rel = ApiQuery::ApplyScope.call(rel, params[:scope])
12
12
 
13
13
  ApiQuery::Search.call(rel, params[:q] || params[:search] || params[:query])
@@ -22,9 +22,19 @@ module Motor
22
22
  variable_name = Regexp.last_match[1]
23
23
 
24
24
  index = selected_variables.index { |name, _| name == variable_name }
25
- selected_variables << [variable_name, variables[variable_name]] unless index
25
+ variable_values = variables[variable_name]
26
26
 
27
- "$#{selected_variables.size}"
27
+ if variable_values.is_a?(Array)
28
+ first_variable_index = selected_variables.size + 1
29
+
30
+ variable_values.each { |value| selected_variables << [variable_name, value] } unless index
31
+
32
+ (first_variable_index..selected_variables.size).map { |i| "$#{i}" }.join(', ')
33
+ else
34
+ selected_variables << [variable_name, variables[variable_name]] unless index
35
+
36
+ "$#{selected_variables.size}"
37
+ end
28
38
  end
29
39
 
30
40
  [rendered, selected_variables]
@@ -7,13 +7,16 @@ module Motor
7
7
 
8
8
  QueryResult = Struct.new(:data, :columns, :error, keyword_init: true)
9
9
 
10
- WITH_STATEMENT_START = 'WITH __query__ AS ('
10
+ CTE_NAME = '__query__'
11
+
12
+ WITH_STATEMENT_START = "WITH #{CTE_NAME} AS ("
11
13
 
12
14
  WITH_STATEMENT_TEMPLATE = <<~SQL
13
15
  #{WITH_STATEMENT_START}%<sql_body>s
14
- ) SELECT * FROM __query__ LIMIT %<limit>s;
16
+ )
15
17
  SQL
16
18
 
19
+ STATEMENT_VARIABLE_REGEXP = /\$\d+/.freeze
17
20
  PG_ERROR_REGEXP = /\APG.+ERROR:/.freeze
18
21
 
19
22
  RESERVED_VARIABLES = %w[current_user_id current_user_email].freeze
@@ -23,11 +26,14 @@ module Motor
23
26
  # @param query [Motor::Query]
24
27
  # @param variables_hash [Hash]
25
28
  # @param limit [Integer]
29
+ # @param filters [Hash]
26
30
  # @return [Motor::Queries::RunQuery::QueryResult]
27
- def call!(query, variables_hash: nil, limit: DEFAULT_LIMIT)
31
+ def call!(query, variables_hash: nil, limit: nil, filters: nil)
28
32
  variables_hash ||= {}
33
+ limit ||= DEFAULT_LIMIT
34
+ filters ||= {}
29
35
 
30
- result = execute_query(query, limit, variables_hash)
36
+ result = execute_query(query, limit, variables_hash, filters)
31
37
 
32
38
  QueryResult.new(data: result.rows, columns: build_columns_hash(result))
33
39
  end
@@ -36,8 +42,8 @@ module Motor
36
42
  # @param variables_hash [Hash]
37
43
  # @param limit [Integer]
38
44
  # @return [Motor::Queries::RunQuery::QueryResult]
39
- def call(query, variables_hash: nil, limit: DEFAULT_LIMIT)
40
- call!(query, variables_hash: variables_hash, limit: limit)
45
+ def call(query, variables_hash: nil, limit: nil, filters: nil)
46
+ call!(query, variables_hash: variables_hash, limit: limit, filters: filters)
41
47
  rescue ActiveRecord::StatementInvalid => e
42
48
  QueryResult.new(error: build_error_message(e))
43
49
  end
@@ -51,10 +57,11 @@ module Motor
51
57
  # @param query [Motor::Query]
52
58
  # @param limit [Integer]
53
59
  # @param variables_hash [Hash]
60
+ # @param filters [Hash]
54
61
  # @return [ActiveRecord::Result]
55
- def execute_query(query, limit, variables_hash)
62
+ def execute_query(query, limit, variables_hash, filters)
56
63
  result = nil
57
- statement = prepare_sql_statement(query, limit, variables_hash)
64
+ statement = prepare_sql_statement(query, limit, variables_hash, filters)
58
65
 
59
66
  ActiveRecord::Base.transaction do
60
67
  result =
@@ -62,6 +69,8 @@ module Motor
62
69
  when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
63
70
  PostgresqlExecQuery.call(ActiveRecord::Base.connection, statement)
64
71
  else
72
+ statement = normalize_statement_for_sql(statement)
73
+
65
74
  ActiveRecord::Base.connection.exec_query(*statement)
66
75
  end
67
76
 
@@ -88,27 +97,68 @@ module Motor
88
97
  # @param query [Motor::Query]
89
98
  # @param limit [Integer]
90
99
  # @param variables_hash [Hash]
100
+ # @param filters [Hash]
91
101
  # @return [Array]
92
- def prepare_sql_statement(query, limit, variables_hash)
102
+ def prepare_sql_statement(query, limit, variables_hash, filters)
93
103
  variables = merge_variable_default_values(query.preferences.fetch(:variables, []), variables_hash)
94
104
 
95
105
  sql, query_variables = RenderSqlTemplate.call(query.sql_body, variables)
106
+ cte_sql = format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.delete_suffix(';'))
107
+ cte_select_sql = build_cte_select_sql(limit, filters)
96
108
 
97
109
  attributes = build_statement_attributes(query_variables)
98
110
 
99
- [format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.delete_suffix(';'), limit: limit), 'SQL', attributes]
111
+ [[cte_sql, cte_select_sql].join, 'SQL', attributes]
112
+ end
113
+
114
+ # @param limit [Number]
115
+ # @param filters [Hash]
116
+ # @return [String]
117
+ def build_cte_select_sql(limit, filters)
118
+ table = Arel::Table.new(CTE_NAME)
119
+
120
+ arel_filters = build_filters_arel(filters)
121
+
122
+ expresion = table.project(table[Arel.star])
123
+ expresion = expresion.where(arel_filters) if arel_filters.present?
124
+
125
+ expresion.take(limit.to_i).to_sql
126
+ end
127
+
128
+ # @param filters [Hash]
129
+ # @return [Arel::Nodes, nil]
130
+ def build_filters_arel(filters)
131
+ return nil if filters.blank?
132
+
133
+ table = Arel::Table.new(CTE_NAME)
134
+
135
+ arel_filters = filters.map { |key, value| table[key].in(value) }
136
+
137
+ arel_filters[1..].reduce(arel_filters.first) { |acc, arel| acc.and(arel) }
100
138
  end
101
139
 
102
140
  # @param variables [Array<(String, Object)>]
103
141
  # @return [Array<ActiveRecord::Relation::QueryAttribute>]
104
142
  def build_statement_attributes(variables)
105
143
  variables.map do |variable_name, value|
106
- ActiveRecord::Relation::QueryAttribute.new(
107
- variable_name,
108
- value,
109
- ActiveRecord::Type::Value.new
110
- )
111
- end
144
+ [value].flatten.map do |val|
145
+ ActiveRecord::Relation::QueryAttribute.new(
146
+ variable_name,
147
+ val,
148
+ ActiveRecord::Type::Value.new
149
+ )
150
+ end
151
+ end.flatten
152
+ end
153
+
154
+ # @param array [Array]
155
+ # @return [Array]
156
+ def normalize_statement_for_sql(statement)
157
+ sql, _, attributes = statement
158
+
159
+ sql = ActiveRecord::Base.sanitize_sql([sql.gsub(STATEMENT_VARIABLE_REGEXP, '?'), attributes.map(&:value)])
160
+
161
+ [sql, 'SQL', attributes]
112
162
  end
113
163
 
114
164
  # @param variable_configs [Array<Hash>]
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.60'
4
+ VERSION = '0.1.61'
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-96c4a62d2fb789ab1080.css.gz": "main-96c4a62d2fb789ab1080.css.gz",
2072
- "main-96c4a62d2fb789ab1080.js.LICENSE.txt": "main-96c4a62d2fb789ab1080.js.LICENSE.txt",
2073
- "main-96c4a62d2fb789ab1080.js.gz": "main-96c4a62d2fb789ab1080.js.gz",
2074
- "main.css": "main-96c4a62d2fb789ab1080.css",
2075
- "main.js": "main-96c4a62d2fb789ab1080.js",
2071
+ "main-fd0f75f789196ce24ffd.css.gz": "main-fd0f75f789196ce24ffd.css.gz",
2072
+ "main-fd0f75f789196ce24ffd.js.LICENSE.txt": "main-fd0f75f789196ce24ffd.js.LICENSE.txt",
2073
+ "main-fd0f75f789196ce24ffd.js.gz": "main-fd0f75f789196ce24ffd.js.gz",
2074
+ "main.css": "main-fd0f75f789196ce24ffd.css",
2075
+ "main.js": "main-fd0f75f789196ce24ffd.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.60
4
+ version: 0.1.61
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-06-15 00:00:00.000000000 Z
11
+ date: 2021-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -169,7 +169,8 @@ files:
169
169
  - lib/motor-admin.rb
170
170
  - lib/motor.rb
171
171
  - lib/motor/active_record_utils.rb
172
- - lib/motor/active_record_utils/active_record_filter.rb
172
+ - lib/motor/active_record_utils/active_record_connection_column_patch.rb
173
+ - lib/motor/active_record_utils/active_record_filter_patch.rb
173
174
  - lib/motor/active_record_utils/active_storage_blob_patch.rb
174
175
  - lib/motor/active_record_utils/active_storage_links_extension.rb
175
176
  - lib/motor/active_record_utils/defined_scopes_extension.rb
@@ -1490,8 +1491,8 @@ files:
1490
1491
  - ui/dist/icons/zoom-money.svg.gz
1491
1492
  - ui/dist/icons/zoom-out.svg.gz
1492
1493
  - ui/dist/icons/zoom-question.svg.gz
1493
- - ui/dist/main-96c4a62d2fb789ab1080.css.gz
1494
- - ui/dist/main-96c4a62d2fb789ab1080.js.gz
1494
+ - ui/dist/main-fd0f75f789196ce24ffd.css.gz
1495
+ - ui/dist/main-fd0f75f789196ce24ffd.js.gz
1495
1496
  - ui/dist/manifest.json
1496
1497
  homepage:
1497
1498
  licenses: