motor-admin 0.1.60 → 0.1.65

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: 5d709fb5b88f56a20cb9c2f906f3540b53278e04becc96598556dba70a0dcb4f
4
- data.tar.gz: 5ffca9ae9451e2aa373957116c1b9b6607f5aba3bc1fa1cee555682c1a2987be
3
+ metadata.gz: b5f4c9fccf887dc60e81cb6bb4d2aacda0fbbf4e9aeac48474ef4c5067263873
4
+ data.tar.gz: 6eb4219e8ae7796991e3973b44daca3ec59d647ef4bb9471796558565ac49541
5
5
  SHA512:
6
- metadata.gz: 944babec34990e1889a52e76f652ca725dda31f4bcbb75aefc47cb715beca9a1a6127d6c5aa1e70437beafdf6a0c9cfaaaf3cf29a901524980812e23134cefaa
7
- data.tar.gz: c79c99d0491e3c02c05296f0ace0ace54bbeeef1cd11f927b49f5edd814503f3e5f0f5a017c184e97bbce9a4b02b7279db36333a8b3fb796fb6125f283e8604a
6
+ metadata.gz: 45bf72ec83403b6ac6fdb02a584d3edd71007f0be8edbd3d58177ca3ed1b4b2f4b527df4bac5d3389bb8f8931234a1498e82ca6ced50c12c1b3c4e6950dfe36e
7
+ data.tar.gz: 7aca3f7e224bc2dc0ed87d8554b769d238ecd8a573f256e080412c9498be33fe58b1b0bc30bb9189d5f0854bf19193ad61e71c18a514ee2b4311a25eeec4f877
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
 
@@ -57,13 +57,14 @@ Custom resource actions can be added via Active Record method call, API endpoint
57
57
 
58
58
  ![Custom form](https://user-images.githubusercontent.com/5418788/119264008-1391dd80-bbea-11eb-9f14-cb405e77fb60.png)
59
59
 
60
- Values from the form fields can be used in API path via `{field_name}` syntax: `/api/some-endpoint/{resource_id}/apply`
60
+ Values from the form fields can be used in API path via `{field_name}` syntax: `/api/some-endpoint/{resource_id}/apply`.<br>
61
+ [Learn more about custom forms builder](https://github.com/omohokcoj/motor-admin/blob/master/guides/building_custom_forms.md).
61
62
 
62
63
  ### SQL Queries
63
64
 
64
65
  ![SQL query](https://user-images.githubusercontent.com/5418788/119264127-84d19080-bbea-11eb-9903-ef465d1d2c97.png)
65
66
 
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.
67
+ 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
68
 
68
69
  ### Data Visualization
69
70
 
@@ -143,12 +144,6 @@ Start example application in development mode:
143
144
  MOTOR_DEVELOPMENT=true rails s
144
145
  ```
145
146
 
146
- ## Comming Soon
147
-
148
- * Multiple databases
149
- * NoSQL data sources
150
- * Pro Bussines intelligence features
151
-
152
147
  ## License
153
148
 
154
149
  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])
@@ -4,6 +4,7 @@ module Motor
4
4
  module ApiQuery
5
5
  module Filter
6
6
  LIKE_FILTER_VALUE_REGEXP = /\A%?(.*?)%?\z/.freeze
7
+ DISTINCT_RESTRICTED_COLUMN_TYPES = %i[json point].freeze
7
8
 
8
9
  module_function
9
10
 
@@ -12,7 +13,10 @@ module Motor
12
13
 
13
14
  normalized_params = normalize_params(Array.wrap(params))
14
15
 
15
- rel.filter(normalized_params).distinct
16
+ rel = rel.filter(normalized_params)
17
+ rel = rel.distinct if can_apply_distinct?(rel)
18
+
19
+ rel
16
20
  end
17
21
 
18
22
  def normalize_params(params)
@@ -47,6 +51,12 @@ module Motor
47
51
  end
48
52
  end
49
53
 
54
+ def can_apply_distinct?(rel)
55
+ rel.columns.none? do |column|
56
+ DISTINCT_RESTRICTED_COLUMN_TYPES.include?(column.type)
57
+ end
58
+ end
59
+
50
60
  def normalize_action(action, value)
51
61
  case action
52
62
  when 'includes'
@@ -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.65'
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-f6cb10fb0e14bdd1703b.css.gz": "main-f6cb10fb0e14bdd1703b.css.gz",
2072
+ "main-f6cb10fb0e14bdd1703b.js.LICENSE.txt": "main-f6cb10fb0e14bdd1703b.js.LICENSE.txt",
2073
+ "main-f6cb10fb0e14bdd1703b.js.gz": "main-f6cb10fb0e14bdd1703b.js.gz",
2074
+ "main.css": "main-f6cb10fb0e14bdd1703b.css",
2075
+ "main.js": "main-f6cb10fb0e14bdd1703b.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.65
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-23 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-f6cb10fb0e14bdd1703b.css.gz
1495
+ - ui/dist/main-f6cb10fb0e14bdd1703b.js.gz
1495
1496
  - ui/dist/manifest.json
1496
1497
  homepage:
1497
1498
  licenses: